导图社区 Java并发总结
Java并发编程虽然难学,但会涉及到操作系统,CPU、内存和其他基本内容,但如果你能坚持学习,内功技能自然会大大提高。本思维导图,涵盖了各个必须掌握的知识点,希望对你有帮助。
编辑于2023-02-07 15:36:03 安徽Java并发总结
基础知识
1. 并发编程的优缺点
1. 为什么要用到并发(优点)
充分利用多核 CPU 的运算能力
方便业务拆分
2. 并发编程的缺点
频繁的上下文切换
线程安全(常见的避免死锁的方式)
3. 易混淆的概念
同步 VS 异步
并发 VS 并行
阻塞 VS 非阻塞
临界区资源
2. 线程的状态和基本操作
1. 如何新建线程
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
几种新建线程方式的比较
2. 线程状态的转换
NEW
RUNNABLE
WAITING
TIMED_WAITING
TERMINATED
BLOCKED
3. 线程的基本操作
interrupt
抛出 InterruptedException 异常时,会清除中断标志位
interrupt(),interrupted(),isInterrupt()
sleep
sleep 和 wait 的区别
join
yield
yield 仅仅只会把时间片让给同优先级的进程,而 sleep 没有这个要求
4. 守护线程 Daemon
并发理论(JMM)
1. JMM 内存模型
1. 哪些是共享数据
实例域
静态域
数组
2. 抽象结构
线程将数据拷贝到工作内存,再刷写到主存,各个线程通过主存中的数据来完成隐式通信。
2. 重排序
1. 什么是重排序
为了提高执行性能,编译器和处理器会对指令进行重排序
针对编译器重排序,编译器重排序规则会禁止一些特定类型的编译器重排序
针对处理器重排序,编译器会在生成指令的时候,插入内存屏障来禁止特定类型的处理器重排序
2. 数据依赖性
3. as-if-serial
遵守 as-if-serial 语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程是按程序的顺序来执行的
3. happens-before 规则
1. 定义
如果 A happens-before B,则 A 操作的结果对操作 B 可见,且 A 操作在操作 B 之前执行
如果指令重排序之后的结果,与按照 happens-before 关系执行的结果一致,则指令可以重排序
2. 理解
站在程序员的角度:为编程人员提供了一个类似强内存的内存结构,方便编程
站在编译器和处理器厂商:在不影响正确结果的前提下,可以让编译器和处理器厂商尽情优化
3. 具体规则
程序顺序规则
volatile 变量规则
监视器锁规则
传递性
start 规则
join 规则
线程中断规则
对象 finnalize 规则
并发关键字
1. synchronized
1. 如何使用
实例方法(锁的是实例对象)
静态方法(锁的是类对象)
代码块根据配置,锁的是实例对象也可以是类对象
2. moniter 机制
字节码中会添加 monitorenter 和 monitorexit 指令
锁的重入性:同一个锁程,不需要再次申请获取锁
3. synchronized 和 happens-before 关系
4. synchronized 的内存语义
共享变量会刷写到主存中,线程每次会从主存中读取最新的值到自身的工作内存中
5. 锁优化
锁状态
无锁状态
偏向锁
轻量级锁
重量级锁
CAS 操作
是一种乐观锁策略
利用现代处理器的 CMPXCHG
存在 ABA 的问题:自旋时间可能过长的问题
Java 对象头
对象的 hashcode
对象的分代年龄
是否是偏向锁的标志位
锁标志位
6. 锁升级策略
轻量级锁
加锁:在对象头和栈帧的锁记录中,添加自身的线程 ID
锁撤销:在全局安全点上进行
轻量级锁
加锁:Displace mark word,对象头 mark word 通过 CAS 指向栈中锁记录
锁撤销:如果 CAS 替换回对象头失败,则升级为重量级锁
重量级锁
各种锁的比较
2. volatile
1. 实现原理
写 volatile 变量在编译时添加 Lock 指令
缓存一致性:每个处理器会通过总线嗅探出自己的工作内存中数据是否发生变化
2. happens-before 关系的推导
3. 内存语义
写 volatile 变量会重新刷写到主存中,其他线程读 volatile 变量,会重新从主存中读取最新值
4. 内存语义的实现
通过在特定位置处插入内存屏障来防止重排序
3. final
1. 如何使用
变量
基本类型
类变量(static 变量):只能在声明时赋值或者在静态代码块中赋值
实例变量:声明时赋值,构造器以及非静态代码块中赋值
局部变量:有且仅有一次赋值机会
引用类型
final 修饰的引用类型只保证引用的对象地址不变,其对象的属性可以改变的
方法
被 final 修饰的方法不能被子类重写,但是可以被重载
类
被 final 修饰的类,不能被子类继承
2. final 重排序的规则
final 域为基本类型
禁止对 final 的写重排序到构造函数之外
禁止读对象的引用和读该对象包含的 final 域重排序
final 域为引用类型
对一个 final 修饰的对象的成员域的写入,与随后在构造函数之外,把这个被构造的对象的引用赋给一个引用变量,这两个变量是不能被重排序的
3. final 的实现原理
插入 StoreStore 和 LoadLoad 内存屏障
4. final 引用不能从构造函数中“溢出”(this 逃逸)
4. 三大特性
原子性
synchronzied
可见性
synchronzied
volatile
有序性
synchronized
volatile
Lock体系
1. Lock 与 synchronized 的比较
Lock 提供了基于 API 的可操作性,提供了可响应中断式 获取锁,超时获取锁以及非阻塞式获取锁的特性
synchronized 执行完同步块以及遇到异常会自动释放锁,而 Lock 要显示的调用 unlock 方法释放锁
2. AQS
1. 设计意图(模板方法设计模式)
AQS 提供给同步组件实现者,为其屏蔽了同步状态的管理,线程排队等底层操作,实现者只需要通过 AQS 提供的模板方法实现同步组件的语义即可
lock(同步组件)是面向使用者的,定义了接口,隐藏了实现细节
2. 如何使用 AQS 实现自定义同步组件
重写了 protected 方法,告诉 AQS 如何判断当前同步状态获取是否成功或者失败
同步组件调用 AQS 的模板方法,实现同步语义。而提供的模板方法又会调用被重写的方法
实现自定义同步组件时,推荐采用继承 AQS 的静态内部类
3. 可重写方法
4. AQS 提供的模板方法
3. AQS 源码解析
1. AQS 同步队列的数据结构
带头节点的双向链表实现的队列
2. 独占式锁
同步状态获取成功则退出;失败时则通过 addWaiter 方法将当前线程封装成节点加入同步队列,acquireQueued 方法使得当前线程等待获取同步状态
3. 共享式锁
锁获取原理
锁释放原理
可响应中断式获取锁以及可超时获取锁特性的实现原理
4. ReetrantLock
1. 重入锁的实现原理
2. 公平锁的实现原理
3. 非公平锁的实现原理
4. 公平锁和非公平锁的比较
5. ReentrantReadWriteLock
1. 如何表示读写状态
低 16 位用来表示写状态,高 16 位表示读状态
2. WriteLock 的获取和释放
当 ReadLock 已经被其他线程获取或者 WriteLock 被其他线程获取,当前线程获取 WriteLork 失败,否则获取成功,支持重入性
WriteLock 释放时将写状态通过 CAS 操作减一
3. ReadLock 的获取和释放
当 WriteLock 已经被其他线程获取的话,ReadLock 获取失败;否则获取成功,支持重入性
WriteLock 释放时将写状态通过 CAS 操作减一
4. 锁降级策略
按照 WriteLock.lock() -> ReadLock.lock() -> WriteLock.unlock() 的顺序,WriteLock 会降级为 ReadLock
5. 生成 Condition 等待队列
WriteLock 可以通过 newCondition 方法生成 Condition 等待队列,而 ReadLock 无法生成 Condition 等待队列
6. 应用场景
适用于读多写少的应用场景,比如缓存设计上
6. Condition 机制
1. 与 Object 的 wait/notify 机制相比具有的特性
Condition 能够支持不响应式中断,Object 不支持
Lock 能够支持多个 Condition 等待队列,而 Object 只能支持一个
Condition 能够支持设置超时时间的 await,而 Object 不能
2. 与 Object 的 wait/notify 相对应的方法
针对 Object 的 wait 方法:await,awaitNanos……
针对 Object 的 notify/notifyAll 方法:signal,signalAll 方法
3. 底层数据结构
复用 AQS 的 Node 类,由不带头节点的链表实现的队列
4. awiat 实现原理
将调用的 await 方法的线程封装成 Node,尾插入到同步队列中,并通过 LockSupport.part 方法,将当前线程置于 WAITING 状态,直至其他线程通过 signal/signalAll 方法将其移入到同步队列中,使其有机会在同步队列中通过自旋获取到 Lock,从而当前线程才能从 await 方法外退出
5. signal/signalAll 实现原理
将等待队列的队头节点移入到同步队列中
6. await 和 signal/signalAll 的结合使用
7. LockSupport
1. 主要功能
可阻塞线程以及唤醒线程,功能实现依赖于 Unsafe 类
2. 与 synchronized 阻塞唤醒相比具有的特色
LockSupport 通过 LockSupport.unpark(thread) 可以指定哪个线程被唤醒,而 synchronized 不能
并发容器
1. concurrentHashMap
1. 关键属性
table:元素为 Node 类的哈希桶数组
nextTable:扩容时的新数组
sizeCtl:控制数组的大小
Unsafe u:提供对哈希桶数组元素的 CAS 操作
2. 重要内部类
Node:实现 Map.entry 接口,存放 key,value
TreeNode:继承 Node,会被封装成 TreeBin
TreeBin:进一步封装 TreeNode,链表过长时转成红黑树时使用
ForwardingNode:扩容时出现的特殊节点
3. 涉及到的 CAS 操作
tabAt:查询哈希桶数组的元素
casTabAt:设置哈希桶数组中索引为 i 的元素
setTabAt:设置哈希桶数组中索引为 i 的元素
4. 构造方法
数组长度总是会保证为 2 的幂次方
5. put 执行流程
1. 如果当前数组还未初始化,先进行初始化
2. spread 方法重哈希(高16位和低16位异或操作),将哈希值与数组长度与运算,确定待插入节点的索引为 i
3. 当前哈希桶的 i 处于 null,直接插入
4. i 处节点不为 null 的话并且节点 hash > 0,说明 i 处为链表头节点。遍历链表,遇到与 key 相同的节点则覆盖其 value,如果遍历完没有找到,则尾插入新节点
5. i 处节点不为 null 的话并且节点状态为 MOVED,则说明在扩容,帮助扩容
6. i 处节点不为 null 的话并且节点为 TreeBin,则使用红黑树的方式插入节点
7. 插入新节点后,检查链表长度是否大于 8,若大于,则转为红黑树
8. 检测数组长度,若超过临界值,则扩容
6. get 执行流程
7. 扩容机制
8. 用于统计 size 的方法的执行流程
9. 1.8 版本的 ConcurrentHashMap 与之前的版本的比较
减少锁粒度
采用了 synchronized 而不是 lock,大量使用 CAS 操作
2. CopyOnWriteArrayList
1. 实现原理
利用了读写分离的思想;当写线程写入数据的时候会复制新建一个新容器,当数据更新完成后,再将旧容器引用指向新容器。读线程感知数据更新是延迟的,也就是说 COW 是牺牲了数据实时性而保证数据最终一致性。 由于写线程写数据是在新容器写入的,因此读线程不会被阻塞。
2. COW 和 ReentrantReadWriteLock 的区别
相同点
1. 两者都采用了读写分离的思想,并且读和读线程之间不会被阻塞
不同点
1. 当写线程在写数据时,ReadWriteLock 会阻塞读线程,而由于 COW 采用了延时更新策略,COW 并不会阻塞读线程
2. ReadWriteLock 保证了数据实时性,而 COW 保证了数据最终一致性
3. 应用场景
适用于读多写少的场景,比如系统的黑名单,白名单等等
4. 为什么具有弱一致性
COW 的实现是采用数组,而数组的引用是 volatile 修饰,但是数组的元素不是 volatile的。因此数据更新只有当 volatile 引用指向新数组时才会生效
5. COW 的缺点
由于在写数据时,会复制,因此可能会出现内存使用瞬间增加,导致 minor GC 和 major GC
只具有数据最终一致性,对数据实时性要求高的场景不合适
3. ThreadLocal
1. 实现思想
采用“空间换时间”的思想,每个线程拥有变量副本,达到隔离线程的目的,线程间不受影响,解决线程安全的问题
操作系统
进程,线程,协程
定义
进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的基本单位。每个进程都有自己的独立内存空间,由于进程比较重量,占据独立的内存,所以上下文进程间切换开销比较大。 线程:线程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位,线程自己基本不拥有系统资源,只拥有一点在运行中不可缺少的资源(如程序计数器,寄存器和栈) 协程:协程是一种用户态的轻量级的线程,协程的调度完全由用户控制。协程拥有自己的寄存器和栈,协程调度切换时,将寄存器和栈保存在其他地方,再切回来的时候,恢复先前保存的寄存器上下文和栈,基本没有内核切换的开销,可以不加锁,所以上下文切换非常快
比较
进程和线程比较: 1. 进程是系统资源分派和调度的基本单位,而线程是 CPU 调度和分派的基本单位 2. 进程拥有自己独立的资源,而线程是进程的一个实体,共享进程资源,自己只拥有极小的资源 线程和协程的区别: 协程是用户态轻量级线程,能由用户控制切换,切换开销小,而线程的切换不能由用户控制并且切换开销较大
2. set 方法原理
数据存放在由当前线程 Thread 维护的 ThreadLocalMap 中,数据结构为当前 ThreadLocalMap 实例为 key,值为 value 的键值对
3. get 方法原理
以当前 ThreadLocal 为键,从当前线程 Thread 维护的 ThreadLocalMap 中获取 value
4. remove 方法原理
从当前线程 Thread 维护的 ThreadLocalMap 中删除以当前 ThreadLocal 实例为键的键值对
5. ThreadLocalMap
底层数据结构
键为 ThreadLocal 实例,值为 value 的 Entry 数组
数组大小为 2 的幂次方
键 ThreadLocal 为弱引用
set 方法原理
1. 计算 ThreadLocal 的 hashcode
总是加上 0x61c88647,这是“Fibonacci Hashing”
2. 计算待插入的索引为 i
采用与运算
3. 如何解决 hash 冲突
当索引为 i 处有 Entry 的话(hash冲突),就采用线性探测,进行环形搜索
4. 加载因子
ThreadLocalMap 初始大小为 16,加载因子为 2/3
5. 扩容 resize
容量为原数组大小的两倍
getEntry 方法原理
根据 ThreadLocal 的 hashcode 进行定位,如果所定位的 Entry 的 key 与所查找的 key 相同则直接返回,否则,环形向后继续探测。
remove 原理
先找到对应的 entry,然后让它的 key 为 null,之后再对其进行清理
6. ThreadLocal 内存泄漏
造成内存泄漏的原因
由于 ThreadLocal 在 Entry 中是弱引用,当外部的 ThreadLocal 实例被置为 null 后,根据可达性分析,堆中 ThreadLocal 不可达,会被 GC 掉,因此就存在 key 为 null 的 entry。无法通过 key 为 null 去访问 entry,因此,就会存在 threadRef -> currentThread -> threadLocalMap -> entry -> valueRef -> valueMemory 引用链造成 valueMemory 不会被 GC 掉,造成内存泄漏
怎样来解决内存泄漏
关键字 cleanSomeSlots,expungeStaleEntry,replaceStaleEntry
在 ThreadLocal 的 set,getEntry 以及 remove 方法中都利用了以上三个关键方法对潜在的内存泄漏进行处理
为什么要使用弱引用
如果使用强引用的话,即使显示对 ThreadLocal 的实例置为 null 的话,由于 Thread,ThreadLocal 以及 ThreadLocalMap 引用链关系,ThreadLocal 也不会被 GC 掉,反而会给程序员带来困扰。
使用弱引用,尽管存在 ThreadLocal 内存泄漏的危险,但实际上已经对其进行了处理
7. ThreadLocal 的最佳实践
使用完 ThreadLocal 后要 remove 掉
8. 应用场景
hibernate 管理 session,每个线程维护其自身的 session,彼此不干扰
用于解决对象不能被多个线程共享的问题
4. BlockingQueue
1. BlockingQueue 的基本操作
2. 常用的 BlockingQueue
ArrayBlockingQueue:由数组实现的有界阻塞队列
LinkedBlockingQueue:由链表实现的有界阻塞队列,可指定长度,如果没有则指定 Integer.MAX_VALUE
PriorityBlockingQueue:支持优先级的无界阻塞队列
SynchronousQueue:不存储任何元素的阻塞队列
LinkedTransferQueue:由链表实现的无界阻塞队列
LinkedBlockingDeque:由链表实现的无界阻塞队列
DelayQueue:存在实现了 Delayed 接口的数据的无界阻塞队列
3. ArrayBlockingQueue 与 LinkedBlockingQueue
会有 notFull 和 notEmpty 两个等待队列,分别存放被阻塞的插入数据线程以及被阻塞的消费数据的线程
ArrayBlockingQueue 只有一个 lock,而 LinkedBlockingQueue 有两个 lock,因此 LinkedBlockingQueue 的并发度更高,吞吐量更大
通过 put 和 take 方法了解生产者-消费者的正确方法
5. ConcurrentLinkedQueue
1. 实现原理
主要采用 CAS 操作以保证线程安全,并且采用了延迟更新的策略,提高吞吐量
2. 数据结构
由 Node 构成的链式队列
3. 核心方法
offer 方法实现原理
poll 方法实现原理
4. HOPS 延迟更新的设计意图
尽可能的减少 CAS 操作,以提升吞吐量和执行效率
线程池(Executor体系)
1. ThreadPoolExecutor
1. 为什么要使用线程池
降低资源损耗
提升系统响应速度
提高线程的可管理性
2. 执行流程
核心线程 corePool,阻塞队列 workQueue 以及最大线程池 maxPool 三级缓存的工作方式
3. 构造器各个参数的意义
coolPoolSize:核心线程的大小
maximumPoolSize:线程池最大容量
keepAliveTime:空闲线程可存活时间
unit:keepAliveTime 的时间单位
workQueue:存放任务的阻塞队列
threadFactory:生成线程的工厂类
handler:饱和丢失策略。共四种:AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy
4. 如何关闭线程池
shutdown:正在执行任务的线程,将任务执行完,空闲线程以中断方式关闭
shutdownNow:停止所有线程,包括正在执行任务的线程。返回未执行的任务列表
showdown 将线程池状态设置为 SHUTDOWN,而 SHUTDOWN 将线程池状态设置为 STOP
isTerminated 来检查线程池是否已经关闭
5. 如何配置线程池
CPU 密集型:Ncpu + 1
IO 密集型:2Ncpu
任务按照 IO 密集型和 CPU 密集型进行拆分
2. ScheduledThreadPoolExecutor
1. UML(类结构)
继承了 ThreadPoolExecutor,并实现了 ScheduledExecutorService
2. 常用方法
可延迟执行任务:schedule(...)
可周期执行任务:scheduledAtFixedRate(...) 和 scheduledWithFixedDelay
scheduledAtFixedRate 和 scheduledWithFixedDelay 的区别:scheduledAtFixedRate 不要求任务结束了才开始统计延迟时间,而 scheduledWithFixedDelay 要求从任务结束开始统计延迟时间
3. ScheduledFutureTask
可周期执行的异步任务,每一次执行完后会重新设置任务下一次执行的任务,并且会添加到阻塞队列中
4. DelayedWorkQueue
按优先级排序的有界阻塞队列,底层数据结构是堆
3. FutureTask
1. FutureTask 的几种状态
未启动(还未执行 run 方法),已启动(已执行 run 方法),已结束(正常结束,被取消,出现异常)
2. get()
未启动和已启动状态,get 方法会阻塞当前线程直到异步任务执行结束
3. cancel()
未启动状态时,调用 cancel 方法后该异步任务永远不会再执行
已启动状态,调用 cancel 方法后根据参数是否中断当前任务的线程
已结束状态,调用 cancel 方法时会返回 false
4. 应用场景
当一个线程需要等待另一个任务执行结束后才能继续进行时,可以使用 futureTask
5. 实现了 Runnable 接口
futureTask 同样可以交由 executor 执行,获取直接调用 run 方法执行
原子操作类
1. 实现原理
借助 Unsafe 类的 CAS 操作,达到并发安全的目的
2. 原子更新基本类型
AtomicInteger,AtomicLong,AtomicBoolean
3. 原子更新数组类型
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
4. 原子更新引用类型
AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference
5. 原子更新字段类型
AtomicIntegerFieldUpdater,AtomicLongUpdater,AtomicStampedReference
并发工具
1. 倒计时器 CountDownLatch
当 CountDownLatch 维护的计数器减至为 0 的时候,调用 await 方法的线程才会继续往下执行,否则会阻塞等待
适用于一个线程需要等待其他多个线程执行结果的应用场景
2. 循环删栏 CyclicBarrier
当一组线程都达到了“临界点”时,所有的线程才能继续往前执行,否则阻塞等待
3. CountDownLatch 和 CyclicBarrier 的比较
1. CyclicBarrier 能够复用,而 CountDownLatch 维护的计数器不可复用
2. CyclicBarrier 会在 await 处阻塞等待,而 CountDownLatch 在 await 出不会阻塞等待
3. CyclicBarrier 提供了例如 isBroken,getNumberWaiting 等方法能够查询当前状态,而 CountDownLatch 提供的方法较少
4. 资源访问控制 Semaphore
适用于对特定资源需要控制能够并发访问资源的线程个数。需要先执行 acquire 方法获取许可证,如果获取成功后线程才能往下继续执行,否则只能阻塞等待;使用完后需要 release 方法归还许可
5. 数据交换 Exchager
为两个线程提供一个同步点,当两个线程都达到同步点后就可以使用 exchager 方法,互相交换数据;如果一个线程先达到了同步点,会在同步点阻塞等待直到另一个线程也到达同步点
并发实践
生产者-消费者问题
1. 使用 Object 的 await/notifyAll 方式实现
使用 Object 的消息通知机制可能存在的问题
notify 过早,wait 线程无法再获取到通知,以至于一直阻塞等待。解决方法:添加状态标志
wait 条件变化。解决方法:使用 while 进行 wait 条件的判断,而不是在 if 中进行判断
“假死”状态:使用 notifyAll 而不是 notify
2. 使用 lock 的 condition 的 await/signalAll 方法实现
3. 使用 blockingQueue 方式实现
由于 BlockingQueue 有可阻塞的插入和删除数据的 put 和 take 方法,因此,在实现上比使用 Object 和 lock 的方式更简洁