注:大多数内容参考了JavaGuide
Java
线程池可以设置的参数
https://javabetter.cn/interview/java-34.html#_28-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0
七个参数:
1.corePoolSize:核心线程数,线程池中始终存活的线程数。
2.maximumPoolSize:
最大线程数,线程池中允许的最大线程数。
3.keepAliveTime:
存活时间,线程没有任务执行时最多保持多久时间会终止。
4.unit:
单位,参数keepAliveTime的时间单位,7种可选。
5.workQueue:
一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。
6.threadFactory:
线程工厂,主要用来创建线程,默认正常优先级、非守护线程。
7.handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。
synchronized和可重入锁的区别
- 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
- 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
- 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
可重入锁怎么实现
线程安全的集合有哪些
- Vector、HashTable
使用synchronized修饰方法保证线程安全
效率低
- ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet
除了1.8的ConcurrentHashMap大多都是用Lock锁
ConcurrentHashMap怎么保证线程安全
https://javaguide.cn/java/collection/java-collection-questions-02.html#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%AB
JDK1.7
的 ConcurrentHashMap 底层采用 分段的数组+链表
实现,JDK1.8 采用的数据结构跟 HashMap1.8
的结构一样,数组+链表/红黑二叉树。
在 JDK1.7
的时候,ConcurrentHashMap
对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment
的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用
synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化)
整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到
Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
CAS是什么
https://javaguide.cn/java/basis/unsafe.html#cas-%E6%93%8D%E4%BD%9C
CAS 即比较并替换(Compare And
Swap),是实现并发算法时常用到的一种技术。CAS
操作包含三个操作数——内存位置、预期原值及新值。执行 CAS
操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS
是一条 CPU 的原子指令(cmpxchg
指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如
compareAndSwapObject、compareAndSwapInt、compareAndSwapLong)底层实现即为
CPU 指令 cmpxchg 。
存在问题:如果在更新过程中,另一个线程也修改了这个变量,但是修改后的值和原值一样,这样就不会发现修改过,可以采用添加版本号或者时间戳的方式避免。
volatile
- 1.保证内存可见性
- 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程读取被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
- 2.禁止指令重排序
- 指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu 是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了内存屏障,在屏障内的代码禁止指令重排序。
Java的内存区域
Java的gc逻辑
https://juejin.cn/post/7123853933801373733
https://javaguide.cn/java/jvm/jvm-garbage-collection.html
内存分配和回收原则
首先分配对象到新生代的Eden区,再次分配对象时仍旧是优先分配到Eden区,如果发现无法满足,则进行一次Minor
gc(也叫Young
gc),将Eden区的对象复制到S区,如果S区无法满足条件则根据空间分配担保将对象复制到老年代,一般老年代可以满足条件,如果不满足条件则进行Full
gc,对新生代和老年代进行一次gc。
较大的对象(大对象就是需要大量连续内存空间的对象(比如:字符串、数组))也会直接进入老年代,避免将大对象放入新生代产生频繁的gc操作。具体什么是大对象,不同垃圾回收器会有不同的设置:
- G1垃圾回收器可以通过设置参数来确定大对象的大小。
- Parallel Scavenge 垃圾回收器,默认情况没有阈值,是根据当前堆内存的情况和历史数据动态决定的。
jvm会给每个对象设置一个年龄,新生代的对象经历一次Minor gc存活下来时,会将年龄增加1,当年龄达到一定阈值时(默认为15,但也和具体的垃圾收集器或者参数设置有关),也会加入到老年代中。
死亡对象判断方法
- 引用计数法
给对象中添加一个引用计数器:
- 有一个地方引用了这个对象时,计数器加1
- 引用失效则减1
- 计数器为0则表示对象不再被使用
该方法实现简单效率高,但是很难解决对象之间循环引用的问题(循环引用的情况下两个对象的计数器都不为0)。
- 可达性分析算法
通过一系列被称为GC
Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路被称为引用链,当一个对象到GC
Roots没有任何引用链的话,则证明此对象是不可用的,需要被回收。
哪些对象可以作为GC
Roots呢:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(native方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
- 引用类型总结
- 强引用:大部分引用都是强引用,垃圾回收器绝不会回收,当内存空间不足时会抛出OOM异常也不会回收强引用的对象。
- 软引用:如果内存足够,则不会回收,如果内存不足则会回收软引用对象的内存。软引用可以用来实现内存敏感的高速缓存。
- 弱引用:只有短暂的生命周期,无论内存是否充足,只要发现了弱引用的对象就会进行回收。
- 虚引用:虚引用并不会决定任何对象的生命周期,对象如果只有虚引用,就和没有引用一样,任何时候都会被回收。
虚引用必须和引用队列联合使用,而软引用和弱引用不必须和引用队列联合使用。
- 如何判断废弃常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量"abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。
- 如何判断无用类
类需要同时满足下面 3 个条件才能算是 “无用的类”:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
垃圾收集算法
- 标记清除算法
分为标记和清除两个阶段:首先标记出所有不回收的对象,标记完成后统一回收掉所有没有被标记的对象。
是最基础的算法,后续算法都是对他的改进,这种算法会有两个明显的问题:
- 效率问题:标记和清除两个过程效率都不高
- 空间问题:标记清楚后会产生大量不连续的内存碎片
- 标记整理算法
标记过程和标记清除算法一样,后续步骤不是直接对可回收对象回收,而是让所有存活的对象向前一端移动,然后直接清理掉端边界以外的内存。由于多了整理的步,效率不高,适合老年代这种垃圾回收频率不是很高的场景。
- 复制算法
解决效率和内存碎片问题。将内存分为大小相同的两块,每次使用其中一块,当一块使用完后,将存活的对象复制到另一块,然后清理使用的空间。
存在两个问题:
- 可用内存变小
- 不适合老年代,如果存活对象数量较多,复制性能会变差。
- 分代收集算法
根据对象存活周期将内存分为几块。一般将Java堆分为新生代和老年代。新生代中每次收集都会有大量对象死去,所以可以选择标记复制算法,只需要少量对象的复制成本就可以完成垃圾收集;而老年代的对象存活几率比较高,而且没有额外的空间担保,所以可以使用标记清除或者标记整理算法。
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现。到目前为止还没有最好的垃圾收集器出现,只能根据具体的应用场景选择合适的垃圾收集器。
JDK默认的垃圾收集器:
- 1.8:Parallel Scavenge(新生代) + Parallel Old(老年代)
- 9 - 20: G1
- Parallel Scavenge收集器
Parallel Scavenge收集器是使用标记复制算法的多线程收集器,该收集器的关注点在于吞吐量(高效率使用CPU),吞吐量指CPU中用于运行用户代码时间与CPU总消耗时间的比值。该收集器提供了很多参数供用户找到合适的停顿时间或最大吞吐量,也可以使用收集器的自适应调节策略。
- Parallel Old收集器
Parallel Scavenge的老年代版本,使用多线程的标记整理算法,同样注重吞吐量。
- G1收集器
G1 (Garbage-First)
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.
以极高概率满足GC 停顿时间
要求的同时,还具备高吞吐量性能特征。
有以下特征:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1收集器大致分为几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1
收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的
Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region
划分内存空间以及有优先级的区域回收方式,保证了 G1
收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
HashMap的原理
https://javaguide.cn/java/collection/hashmap-source-code.html
jdk1.8之前由
数组+链表
组成,链表主要为了解决哈希冲突(拉链法)。在jdk1.8之后解决哈希冲突首先使用链表,当链表长度大于等于阈值(默认为8)时,首先判断数组长度,如果数组长度小于64,会选择扩容,如果数组长度大于64,会将链表转成红黑树,减少搜索的时间。
刚刚创建HashMap
对象时数组长度为0,当第一次插入数据时数组长度设置为16。
首先将对象的hashCode
和hashCode
无符号右移16位的结果做异或(添加一些扰动)获得实际的哈希值(设为hash
),然后将hash
和数组长度-1做与运算(等同于取模,因为数组长度为2的n次幂)得到存放的数组索引位置。
当键值对的数量size
超过阈值(数组长度*负载因子(默认是0.75))就会对数组进行扩容。
悲观锁和乐观锁的区别、场景
https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html