Java基础知识

Java Object类方法

equals,hashcode,toString
wait,notify,notifyAll
finalize,clone

HashCode 作用,和 Equals 方法关系?如何重载hashCode方法

很多结构用到了hashcode,比如 HashMap,使用 hashcode 会提高查找速度
想要重写 equals 方法必须同步重写 hashcode 方法,因为 equals 要求 A.equals(B),那么 A.hashcode == B.hashcode,如果重写了 equals 会导致不同对象 equals 相等但是 hashcode 不同,会对 hashmap 等结构造成混乱

Java反射机制

Java代理模式

Java泛型,如何实现,引入泛型的优点?泛型擦除是什么?

泛型优势:

  1. 类型检查,编译时就能检查出问题
  2. 语义化
  3. 自动类型转换

泛型擦除

Java 的泛型是[伪泛型],为了兼容1.5之前的版本,所有泛型类型都会被 Object 取代,并在使用时自动强转,强转失败会报错,同时我们也可以通过 A.getClass().getName() 获取类型信息

  1. public static <T> void check(T t) {
  2. System.out.println("t.getClass() = " + t.getClass());
  3. System.out.println("t.getClass().getName() = " + t.getClass().getName());
  4. }
  5. //输出
  6. //t.getClass() = class practive.Demo$Apple
  7. //t.getClass().getName() = practive.Demo$Apple

上界通配符 —> 具有协变性 —> 限制:只出不入(只能访问,不能修改添加)—> Kotlin 中的 out

  1. List<Apple> apples = new ArrayList<Apple>();
  2. List<? extends Fruit> fruits = apples;

下界通配符 —> 具有逆变性 —> 限制:只入不出(只能修改添加,不能访问)—> Kotlin 中的 in

  1. List<Fruit> fruits = new ArrayList<Fruit>();
  2. List<? super Apple> apples = fruits;

对比 Kotlin

  1. open class Fruit
  2. class Apple : Fruit()
  3. lateinit var apple1: MutableList<in Fruit>
  4. lateinit var apple2: MutableList<out Fruit>
  5. fun AAA() {
  6. val f1: Any? = apple1[0] //只能用 Any 接收,用 Fruit 接收报错
  7. apple1[0] = Fruit() //正常,可赋值
  8. val f2: Fruit = apple2[0] //正常,可以用 Fruit 接收
  9. apple2[0] = Fruit() //报错
  10. }

内部类有哪些?静态内部类和普通内部类的区别(对比Kotlin)

普通内部类

  1. 内部不能有 static 的变量或方法
  2. 外部类可通过内部类对象调用内部类所有方法;内部类可以直接调用外部类的所有方法(权限修饰符无关,因为都是自己内部)

静态内部类

  1. 内部可以有 static 方法或变量(静态内部类完全就是一个单独的类,所以没有什么限制)
  2. 外部类可通过内部类对象调用内部类所有方法;内部类则无法直接调用外部类方法,需要new一个外部类对象

    匿名内部类在使用外部类参数时需要声明final,为什么?如果是基本类型也需要final吗?

    匿名内部类在编译后,会生成一个单独的class文件,编译器为匿名内部类对象生成了一个构造方法,参数就是匿名内部类所使用的外部方法的参数,Java 基本类型是值传递,引用类型是引用传递,如果不加 final,会导致基本数据类型改动对外部无效,引用类型赋值对外部无效,所以为了避免语义歧义,所以必须加 final,JDK1.8已经不需要显示写 final 修饰符,编译器会给你加上

    JVM相关知识

    JVM内存模型,new一个对象,内存如何分配?

    JVM运行时数据区域分成5个:

  3. 程序计数器:用来保存字节码指令

  4. JAVA虚拟机栈:生命周期和线程相同,方法的调用相当于栈帧入栈出栈
  5. 本地方法栈:Native的方法栈
  6. Java堆:存放对象实例
  7. 方法区:存储虚拟机已加载的类信息,常量,静态变量,即时编译器编译后的代码

对象创建过程:
首先执行类加载过程,在检查通过后,虚拟机为新生对象分配内存,内存大小在类加载完后便可以完全确定,根据内存空间是否完整,分为“指针碰撞”和“空闲列表”两种分配内存方式,分配完成后,虚拟机为其初始化零值,再初始化对象头,紧接着调用方法为对象分配初始值

四大引用知道吗?分别说说他们的作用

  1. 强引用:永远不会被回收
  2. 软引用:SoftReference,直到内存要溢出才回收
  3. 弱引用:WeakReference,只存活到下一次垃圾收集发生之前
  4. 虚引用:PhantomReference,无法通过虚引用取得一个实例,为对象设置虚引用关联的唯一目的就是能在被回收时收到一个系统通知

    GC机制

    垃圾收集算法:

  5. 标记-清除算法:标记一遍然后删除,缺点是效率不高,而且产生大量内存碎片

  6. 复制算法:内存空间分成一半,一次只用一半,满了以后把活着的对象复制到另一半,缺点是空间浪费一半、
    1. 此方法多用来回收新生代,内存分成大的 Eden 区和两块小的 Survivor 区,每次使用 Eden 区和一块 Survivor 区,每次收集把两者活着的对象放入另一块 Survivor 区
  7. 标记-整理算法:标记后,把活着的整理到一起
  8. 分代收集算法:堆分成新生代和老年代

    1. 新生代:复制算法
    2. 老年代:标记-整理算法

      GC Root,为什么他们是GC root而不是其他的?

      GC root包含以下几种:
  9. 虚拟机栈(栈帧中的本地变量表)中引用的对象

  10. 方法去中类静态属性引用的对象
  11. 方法去中常量引用的对象
  12. 本地方法栈中JNI引用的对象

    JVM类加载机制

    类加载过程分为三个阶段

  13. 加载阶段 —-> 查找并加载类的class二进制文件,最终产物就是堆内存中的class对象(方法区存A类的数据结构)

  14. 连接阶段
    1. 验证 —->
    2. 准备 —-> 为类的静态变量分配内存,并为其初始化
    3. 解析 —-> 把类中符号转换为直接引用
  15. 初始化阶段 —-> 执行 () 方法为类的静态变量赋初值

    JVM类加载器

  16. 根加载器:Bootstrap ClassLoader:C++编写,负责虚拟机核心类库的加载,java.lang就是他加载的

  17. 扩展类加载器:Ext ClassLoader:JAVA编写,java.lang.URLClassLoader 的子类,负责加载 JAVA_HOME/jre/lb/ext 中的类库,可通过 java.ext.dirs 来得到
  18. 系统类加载器:负责加载 classpath 下的类库,可通过 java.class.path 获取

双亲委托机制(又称 父委托机制)
底层ClassLoader先检查缓存,如没有,再委托最上层ClassLoader去加载,如上层不能加载则自己加载,以此类推…

通过重写 ClassLoader 的 loadClass 方法可以破坏默认的双亲委派模型

举例:自定义的CustomerClass中调用了String类,且CustomerClass是由自定义ClassLoader加载的,那么由于String也是经由自定义ClassLoader最终由系统类加载器记载,但是由于经过了自定义ClassLoader,所以自定义ClassLoader加载的CustomerClass类可以正常使用String类

Java集合类相关知识

ArrayList与LinkList区别与联系

HashMap原理

HashMap中实际存储的结构是 Node 数组,每个值都封装在Node中
初始大小 16,阈值为 0.75f,超过此值会导致扩容

扩容操作就是把数组大小乘以2(必须保证table.length是2的倍数)如果遇到链表情况,循环链表,通过 e.hash & oldCap == 0 来判断当前 Node 是否应该在原地还是新位置,如果

put时,通过计算

  1. int hash = key.hashcode ^ (key.hashcode >>> 16);
  2. index = hash & (tab.length - 1)
  3. //index 就是 table 数组的 index

得到相应的index,如果链表大于8个,就会转化成红黑树

Hash冲突

hash冲突解决方案:

  1. 线性探测 — 往后移一位
  2. 再平方探测 — 加 1 的平方个单位,再减 1 的平方的单位,随之是 2 ,3 的平方
  3. 伪随机探测 — 加上随机数
  4. 链地址法 — HashMap 的方法

    HashMap多线程为什么不安全

    JDK1.7中,扩容操作会导致链表逆序,在并发情况下,在扩容时可能产生循环链表的情况,在调用get()方法是就会死循环
    该问题在JDK1.8上被修复了,1.8是用两个局部变量保存新的链表,都循环完毕后,再放入table中

    HashMap 和 HashTable 区别

    HashTable 是线程安全的 map,默认容量11,扩容时 2 * oldCap + 1,取hash为 (key.hashcode() & 0x7FFFFFFF) & length,插入链表时为头插法(HashMap是尾插法),HashTable 的 key 和 value 都不允许是null(HashMap都允许),线程安全是因为方法上都有 synchronized 修饰

    LinkedHashMap

    继承自 HashMap,内部 Entry 添加了 before 和 after,所以算是双向链表,LinkedHashMap 提供了 访问顺序插入顺序 两种排序方式

ConCurrentHashMap

1.8 之前是分段锁
1.8 之后本质就是 HashMap
并发策略:
读操作:无同步机制,因为有 volatile 修饰 Node 节点的 val 和 next,其他线程的修改是可见的
写操作:当对应位置为 null 时,利用 CAS 插入,否则进行 synchronized 然后在链表或者红黑树上插

线程相关知识

进程和线程是什么,关系?

线程创建方式有哪些

  1. 继承Thread类
  2. 在Thread构造方法中传入Runnable
  3. ThreadFactory
  4. Executor
  5. Callable — 有返回值的Runnable

    Future future = executor.submit(callable); String result = future.get()

线程池的原理?有哪些系统提供的线程池?分别适用场景是什么?

构造方法说明:
ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,BlockingQueue,ThreadFactory,RejectedExecutionHandler)

  1. corePoolSize是默认创建的核心线程数,如果任务超过了这个值,就创建新的非核心线程,最大为maximumPoolSize,在没有任务时且过了keepAliveTime,非核心线程被回收

系统提供4中线程池:

  1. Executors.newFixedThreadPool() 固定线程数,适合处理爆发式任务,处理完关闭
  2. Executors.newCachedThreadPool() 只有非核心线程,最大线程数无线,适合处理大量耗时较少的任务
  3. Executors.SingleThreadExecutor() 只有一个核心线程
  4. Executors.newScheduledThread() 只有核心线程,非核心无限,适合处理周期性的任务

线程池原理:
对于加入的任务,先创建核心线程执行,如果核心线程满,则加入任务队列;如果队列满了,则创建非核心线程,如果两个都满了,则reject
相关类说明:
<> —-
— 封装类,包含4种线程池
shutDown()
清除所有空闲的 worker 也就是只留下工作的线程继续工作,同时 queue 不清空。但是不接受新的请求了
shutDownNow()
清除所有 worker 也就是一个不留,同时 queue 全部清空,也不接受新的请求了

synchronized原理

  1. 提供了一种锁的机制,能够保证共享变量的互斥访问
  2. synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能够保证任何时候任何线程执行到enter成功之前都必须从主内存中的获取数据而非缓存,且exit成功后,共享变量被更新后的值必须刷入主内存
  3. 每个线程想要获得monitor所有权时,monitor计数器就会加一,如果获取了锁的线程重入,则再加一,其他线程直到线程计数器为0时,才能尝试获取monitor的所有权

synchronized 具有锁升级的机制,由 偏向锁—轻量级锁(CAS)—重量级锁 构成
保证原子性,有序性,可见性
x++不是原子操作,实际上可以分成:

步骤1: int temp = x + 1; 步骤2: x = temp;

如果x = 1且执行步骤1时切换成另一个线程,线程2将x赋值为5,再回到线程1,执行步骤2,此时x = 2,出现了错误!!!这时候就要用synchronized保证原子性!

synchronized关键字修饰普通方法和静态方法的区别?

类锁:synchronized 修饰 static 方法或者声明 AA.class
this锁:synchronized 修饰普通方法或者声明 this

synchronized的缺陷以及Lock

synchronized 修饰的方法不可被中断,且无法控制阻塞时长

ReentrantLock

可重入锁 内部利用 CAS 实现同步,可实现公平锁和非公平锁(默认)

方法锁、对象锁、类锁的意义和区别

Volatile实现原理(DCL中,为什么要加上volatile关键字?)

保证可见性,有序性(禁止指令重排序),不保证原子性
多个线程操作同一个变量时,需要加volatile关键字
原因分析:线程都有私有空间,变量是放在堆上(公有)的,每次读写时,线程会把变量的值拷贝到私有域,操作后再更新会堆,如果不加volatile,那么线程2更改了值后,线程1是不知道变量值被更改了的,造成错误。如果加了volatile,那么任何线程读写变量时,都会去堆中拿最新的,而不是一直使用自己的备份

DCL中,线程1执行到 instance = new Singleton();此时还没释放锁就切换到线程2,线程2判断 instance != null 就返回实例调用内部 socket 变量,但此时由于指令重排序,会发成内部变量还没有初始化的问题,多线程下调用会空指针,所以加上 volatile 禁止指令重排序,保证 instance 实例化时,内部成员变量已经完成初始化

  1. class Singleton {
  2. private String s;
  3. private Socket socket;
  4. private Singleton() {
  5. s = "";
  6. socket = XXX;
  7. //由于指令重排序,
  8. }
  9. }

死锁的原因和种类

  1. 交叉锁导致死锁
  2. 内存不足,两个线程都在等对方释放资源
  3. 一问一答式的数据交换;如果错过了某次请求,两边都在空等对方回复
  4. 数据库锁,忘记关闭

    wait 和 sleep 区别

    不同点:

  5. wait 释放锁,sleep不释放锁

  6. wait 是object 的方法,sleep 是 Thread 的方法
  7. wait 必须在同步方法中进行,sleep 不需要
  8. wait 需要被唤醒,sleep不需要

相同点:

  1. 都会阻塞当前线程
  2. 都是可中断方法,被中断会抛出中断异常

    生产者消费者模型

    要点1:用 synchronized 修饰
    要点2:用 while 循环判断
    要点3:用 wait 和 notifyAll 配合

    线程同步的方法:Synchronized、lock、reentrantLock分析

    Java锁的种类: 公平锁、乐观锁、互斥锁、分段锁、偏向锁、自旋锁等

  3. 乐观锁 — 认为一个线程修改数据时另一个线程不会改,所以不加锁

  4. 悲观锁 — 上述相反,例如 synchronized
  5. 排他锁(写锁) / 共享锁(读锁) — 读写锁ReentrantReadWriteLock中的读锁ReadLock是共享锁,写锁WriteLock是排他锁。
  6. 可重入锁 — ReentrantLock,synchronized 同一个线程不同方法用同一个锁,那么可以直接进入无需等待
  7. 公平锁 / 非公平锁 — 多个线程竞争锁时,需不需要排队
  8. 偏向锁 & 轻量级锁 & 重量级锁
  9. 自旋锁 — 线程切换的代价很大而同步代码很短,那我们可以在线程想获取锁时自己空循环几次,不断地盯着当前加锁的线程

ThreadLocal的原理和用法,为什么ThreadLocal能保证一个线程只有一个Looper对象?

ThreadLocal.set(T value) 方法中,先从当前 Thread 中拿到 ThreadLocalMap,再往这个 map 中放入值
get() 方法同理,先从当前 Thread 中拿到 ThreadLocalMap,再从这个 map 中根据当前 ThreadLocal 对象作为 key 取值
ThreadLocalMap 是 ThreadLocal 的静态内部类,内部存了一个 Entry[], Entry 继承了 WeakReference,所以 ThreadLocal 对象作为 key 是弱引用

那为什么要用弱引用呢?因为如果 Thread 生命周期很长,而 ThreadLocal 生命周期短,会造成 ThreadLocal 对象无法释放
但是即使如此,还是可能会有内存泄漏,因为 value 是强引用,当 ThreadLocal 作为 key 被释放,ThreadLocalMap 会出现 key == null ,value 依然有值的情况,造成内存泄漏
ThreadLocalMap 提供了 expungeStaleEntry 方法去除 key == null 的键值对,但是也需要我们主动调用下 ThreadLocal 的 remove 方法

线程同步的方法?

  1. synchronized
  2. wait / notify
  3. ReentrantLock
  4. LinkedBlockingQueue — 内部用 ReentrantLock 加锁,队列满则 wait,加入元素则 signal
  5. util.concurrent.atomic