type
status
date
slug
summary
tags
category
icon
password
一、Java 线程基础
1、线程创建
Thread
• start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行
Runnable
Thread 类本身也是实现了 Runnable 接口,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用
Runnable#run:- 直接使用 Thread 是重写了 run 方法
Thread 与 Runnable 对比
- Thread 是把线程和任务合并在了一起,Runnable 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系(解耦),更灵活(组合优于继承)
- 同一个 Runnable 线程任务对象可以被包装成多个线程对象,适合多个多个线程去共享同一个资源
Callable
- FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成 FutureTask 对象
public V get():同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步- get() 线程会阻塞等待任务执行完成
- run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值
2、线程方法
API
Thread 类 API:
方法 | 说明 |
public void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 |
public void run() | 线程启动后调用该方法 |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字
线程存在默认名称:子线程是 Thread-索引,主线程是 main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行
Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
start 与 run
- run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行
- start:使用 start 是启动新的线程,此线程处于就绪 Runnable(可运行)状态,通过新的线程间接执行 run 中的代码
说明:线程控制资源类
run() 方法中的异常不能抛出,只能 try/catch
- 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
- 异常不能跨线程传播回 main() 中,因此必须在本地进行处理
sleep 与 yield
sleep:
- 调用 sleep 会让当前线程从
Running进入Timed Waiting状态(阻塞)
- 调用方式:Thread.sleep()
- sleep() 方法的过程中,会放弃 CPU 资源,锁资源不会释放
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
- 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用,从
Running进入Runnable就绪状态
- 调用方式:Thread.yield()
- 具体的实现依赖于操作系统的任务调度器
- 会放弃 CPU 资源,锁资源不会释放
join
public final void join():等待这个线程结束
原理:调用者轮询检查线程 alive 状态,t1.join() 等价于:
- join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁
interrupt
public void interrupt():打断这个线程,异常处理机制
public static boolean interrupted():判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted():判断当前线程是否被打断,不清除打断标记
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
1、打断阻塞(sleep/wait/join)
- 会清除打断标记,即置为 false
2、打断正运行线程
- 不会清除打断标记,即仍为 true
3、打断 park
- park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
- 如果打断标记已经是 true, 则 park 会失效
- 将 isInterrupted 方法改为 interrupted 方法,会将标记重置为false,即后面的 park 会生效
4、两阶段终止模式
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2?
错误思想:
- 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式图示:

代码核心实现:
- 阻塞时被打断:会重置标记为false,需在抛异常后重置标记为true
- 正常运行时被打断
daemon
public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程线程启动前调用此方法
- 用户线程:平常创建的普通线程
- 守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
常见的守护线程:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
不推荐
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
public final void stop():停止线程运行
废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面
public final void suspend():挂起(暂停)线程运行
废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
public final void resume():恢复线程运行
3、线程状态
进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态
在 API 中
java.lang.Thread.State 这个枚举中给出了六种线程状态:线程状态 | 导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start() 方法,只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法)
包括:运行、就绪以及操作系统层面的IO阻塞 |
Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态,方法:synchronized(obj) 获取对象锁失败
Java中阻塞概念 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒,方法:join()、LockSupport.park()
Java中阻塞概念 |
Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep(long n) 、Object.wait(long n)、t.join(long n)
Java中阻塞概念 |
Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |

- NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE
- RUNNABLE <—> WAITING:
- 调用 obj.wait() 方法时,t 线程从 RUNNABLE —> WAITING
- 调用 obj.notify()、obj.notifyAll()、t.interrupt():
- 竞争锁成功,t 线程从 WAITING —> RUNNABLE
- 竞争锁失败,t 线程从 WAITING —> BLOCKED
- 当前线程调用 t.join() 方法,注意是当前线程(例如main线程)在 t 线程对象的监视器上等待
- 当前线程调用 LockSupport.park() 方法,当前线程从 RUNNABLE —> WAITING
- RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)
- RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败,进入锁对象的 entryList 阻塞
二、管程
- 阻塞式的解决方案:synchronized,lock
- 非阻塞式的解决方案:原子变量
0、线程安全分析
成员变量和静态变量:
- 没有共享,线程安全
- 被共享
- 只读,线程安全
- 读写,非线程安全
局部变量:
- 局部变量线程安全
- 局部变量引用的对象不一定线程安全
- 没有逃离方法作用域,线程安全
- 逃离方法作用域,非线程安全
- 例如:局部变量引用的对象被子类重写方法进行了写操作
controller,service,dao:
- 若无状态,则线程安全
1、synchronized
锁使用
互斥和同步都可以采用 synchronized 关键字来完成,区别:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized 是可重入、不公平的重量级锁
- 同步代码块
- 对象锁
- 类锁
- 同步方法
- 静态方法 class
- 非静态方法 this
- synchronized 修饰的方法的不具备继承性,所以子类是线程不安全的,如果子类的方法也被synchronized 修饰,两个锁对象其实是一把锁,而且是子类对象作为锁
锁原理
1、Java 对象头
- mark word + class 指针

2、mark word
- 32 位虚拟机

- 64 位虚拟机

3、Monitor
- 管程,监视器,操作系统里的概念
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁

工作流程:
- 开始时 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 存入线程栈中的锁记录中(轻量级锁部分详解)
- 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
- Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
- 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
- WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)
注意:
- synchronized 必须是进入同一个对象的 Monitor 才有上述的效果
- 不加 synchronized 的对象不会关联 Monitor,不遵从以上规则
- 线程获取到锁和释放锁会有obj对象头mark word 与线程栈帧锁记录数据的交换
4、字节码
说明:
- 通过异常 try-catch 机制,确保一定会被解锁
- 方法级别的 synchronized 不会在字节码指令中有所体现
原始代码:
字节码:
锁升级过程
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 随着竞争的增加,只能锁升级,不能降级

轻量级锁
- 一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)
- 可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁
- 轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化
流程展示:
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word(01无锁,00轻量级锁)

- 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁

- 如果 CAS 失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数
- 每次重入都会添加一个锁记录,锁记录个数就是重入次数
- 除了第一个锁记录会存储对象头 mark word 外,其他锁记录只会存储 null

- 当退出 synchronized 代码块(解锁时)
- 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录(即释放这个锁记录内容),表示重入计数减 1
- 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
重量级锁(锁膨胀)
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

- Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

- 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
偏向锁
轻量级锁没有竞争时,每次重入都要进行 CAS 操作,因此 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
- 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
XX:BiasedLockingStartupDelay=0来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低
- 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了
- 添加 VM 参数
XX:-UseBiasedLocking禁用偏向锁
撤销偏向锁的状态:
- 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify,需要申请 Monitor,进入 WaitSet
批量撤销:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
- 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
- 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁优化-自旋锁
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
- 自旋失败的线程会进入阻塞状态
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
自旋锁说明:
- 在 Java 6 之后自旋锁是自适应的,若一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
锁优化-锁消除
- 可使用 jvm 参数 -XX:-EliminateLocks 关闭锁消除优化(默认开启)
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除
锁优化-锁粗化
对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
多把锁
将锁的粒度细分:
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
需求:一个房间既要睡觉又要学习一把锁锁房子不合适,可以使用两把锁,各锁各的,就不会有问题。
活跃性
死锁
Java 死锁产生的四个必要条件:
- 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路
定位死锁
使用 jps 定位进程 id,再用
jstack id 定位死锁,找到死锁的线程去查看源码,解决优化
- Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用
top -Hp 进程id来定位是哪个线程,最后再用 jstack <pid>的输出来看各个线程栈
- 避免死锁:避免死锁要注意加锁顺序
- 可以使用 jconsole 工具,在
jdk\bin目录下
活锁
活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程
解决方法:睡眠时间错开即可,或者随机随眠时间
两个线程互相改变对方的结束条件,最后谁也无法结束:
饥饿
饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
2、wait & notify
需要获取对象锁后才可以调用
锁对象.wait(),若无锁直接调用会抛出异常 `IllegalMonitorStateException`。
基本 API
方法都是 final 的,不会被重写。
- notify:从 monitor 的 waitSet 随机唤醒一个
- notifyAll:从 monitor 的 waitSet 唤醒全部
- wait:使线程进入 monitor 的 waitSet 等待,直到调用 notify 方法唤醒
- wait(n):有时限等待,超过时间未被唤醒,不会继续等待,会继续进入 entryList 等待抢占锁
说明:被唤醒的线程不会立马获得锁,需要进入 entryList 去抢占锁,即 owner
底层原理

- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争
与 Sleep 对比
- 类所属不同:sleep() 方法是属于 Thread 类,wait() 方法属于 Object 类,用于线程间通信
- 锁处理机制不同:sleep() 方法不会释放对象锁, wait() 方法线程会放弃对象锁,进入锁对象 monitor 的 waitSet;都会释放 CPU
- 使用区域不同:wait() 方法必须放在同步控制方法和同步代码块(先获取锁)中使用,sleep() 方法则可以放在任何地方使用
- 相同点:线程状态都是 TIMED_WAITING
虚假唤醒及解决优化
虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其他线程也在等待,那么就可能唤醒不了正确的线程
解决方法:采用 notifyAll 来确保能把需要唤醒的线程唤醒
注意:如果使用 if 不使用 while 就会导致非目标线程一次判断条件不符合就结束
代码示例
使用模板
3、join 原理
join 就是使用了保护性暂停模式,只不过:
- join 是等待另一个线程结束,而保护性暂停时等待另一个线程结果
- 同样 while 防止虚假唤醒
4、park & unpark
基本使用
LockSupport 是用来创建锁和其他同步类的线程原语
LockSupport 类方法:
LockSupport.park():暂停当前线程,挂起原语
LockSupport.unpark(暂停的线程对象):恢复某个线程的运行
与 wait notify 对比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
- park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify。
- wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU
原理
调用的都是
Unsafe 类的方法。每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
类似生产者消费者,counter 为 1 就不会阻塞,而是进行消费,然后将 counter 置为0
park & unpark 可以先 park 也可以先 unpark:
先 park:
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
- 线程进入 _cond 条件变量挂起
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0


先 unpark:
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0

5、同步模式
单任务保护性暂停
Guarded Suspension,用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式

代码实现:添加超时机制
- 已知传入的最大等待时间 millis,需要积累开始时间 begin、已经等待时间 timePassed
- 为防止 while 循环多等待,因此需要再进入 while 后首先判断还需要等待的时间 millis - timePassed,不需要等待则直接 break
- 若还没有到最大等待时间则继续等待 millis - timePassed
- 若在 wait 时被提前唤醒,条件还不满足则继续等待 millis - timePassed
多任务保护性暂停

多任务一对一的结果传递,可使用此模式。
需求:
- 居民:既充当写信人也充当收信人
- 邮箱:存放邮件
- 邮员:从邮箱取对应邮件,一对一
- 邮件:response 结果,传递的资源
居民:
邮员:
邮箱:
邮件:
测试方法:
顺序输出
按照指定顺序输出一次;
交替输出
三个线程交替打印:
- 用一个三种状态的标记标识即可
- 上一个线程执行完毕后将标记改为下一个要执行的标记即可
wait notify 实现
await signal 实现
- 三个条件变量可代替 wait notify 的 flag 标记
- 刚开始三个线程分别在对应条件队列等待
- 一秒后主线程唤醒 a 开始循环
park unpark 实现
- 三个线程先 park 等待
- 一秒后主线程唤醒 a 线程进行循环
6、异步模式
生产者消费者
同步模式之保护性暂停不论是单任务还是多任务生产者和消费者线程都是一对一的;
而生产者消费者模式是多对多的。
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
实现一个三个生产者一个消费者的代码:
三、内存
1、JMM 内存模型
是一种抽象的概念,实际上并不存在。线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成。
- 主内存:存储所有共享变量的值
- 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝
主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存
2、可见性
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题
解决方案:
- 使用
volatile关键字修饰变量即可,可强制线程去主存读取最新值;轻量级操作
- 或使用
synchronized进行加锁,重量级操作
3、原子性
原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响。
解决方案:加锁
4、有序性
有序性:指令按序执行
指令重排:Java内存模型允许编译器和处理器对指令进行重排序,以提高性能。这可能导致看似有序的操作实际上在不同线程中执行的顺序被打乱
指令执行过程:源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令
现代 CPU 支持多级指令流水线,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为五级指令流水线。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率
解决方案:使用
volatile 禁用指令重排5、volatile
概述
volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)
- 保证可见性
- 不保证原子性(指令交错)
- 保证有序性(禁止指令重排)
性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小
synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性
- 有序性:加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
- 可见性:线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中
原理
使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据
lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
内存屏障有三个作用:
- 确保对内存的读-改-写操作原子执行
- 有序性:阻止屏障两侧的指令重排序
- 可见性:强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效
保证可见性:
- 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据
保证有序性:
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
不能解决指令交错(原子性):
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前
- 有序性的保证也只是保证了本线程内相关代码不被重排序
6、happen-before
Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结
不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性
- 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序
- 锁定规则 (Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见
- volatile 变量规则 (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读
- 传递规则 (Transitivity):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C
- 线程启动规则 (Thread Start Rule):Thread 对象的 start()方 法先行发生于此线程中的每一个操作
- 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行
- 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始
7、两阶段终止模式改进
停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
四、无锁
1、CAS
简介
CAS 的全称是 Compare-And-Swap(Set),是 CPU 并发原语
- CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作
- CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的
底层原理:CAS 的底层是
lock cmpxchg 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性- 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
- 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性
特点
- CAS 体现的是无锁并发、无阻塞并发,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
- CAS 是基于乐观锁的思想
- CAS 由于要获取共享变量的最新值,因此共享变量一定要使用 volatile 修饰,CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
缺点
- 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,使用 CAS 线程数不要超过 CPU 的核心数,采用分段 CAS 和自动迁移机制
- 竞争激烈重试必然频繁发生,效率会有影响
- 只能保证一个共享变量的原子操作
- 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
- 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
- 会有 ABA 问题
2、原子类 Atomic
原子整数
常见原子类:AtomicInteger、AtomicBoolean、AtomicLong
构造方法:
public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer
常用API:
方法 | 作用 |
public final int get() | 获取 AtomicInteger 的值 |
public final int getAndIncrement() | 以原子方式将当前值加 1,返回的是自增前的值 |
public final int incrementAndGet() | 以原子方式将当前值加 1,返回的是自增后的值 |
public final int getAndSet(int value) | 以原子方式设置为 newValue 的值,返回旧值 |
public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回
实例:AtomicInteger 里的 value |
原子引用
原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量
原子引用类:AtomicReference、AtomicStampedReference(增加版本号解决ABA问题)、AtomicMarkableReference(简化版本号,使用boolean值标记)
AtomicReference 类:
- 构造方法:
AtomicReference<T> atomicReference = new AtomicReference<T>()
- 常用 API:
public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作public final void set(V newValue):将值设置为 newValuepublic final V get():返回当前值
原子数组
原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
AtomicIntegerArray 类方法:
原子更新器
原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
IllegalArgumentException: Must be volatile type常用 API:
static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName):构造方法
abstract boolean compareAndSet(T obj, int expect, int update):CAS
原子累加器
原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
LongAdder 和 LongAccumulator 区别:
相同点:
- LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的
- LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr
不同点:
- 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x)
- LongAccumulator 类功能更加强大,构造方法参数中
- accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则
- identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的 0
3、原子累加器 Adder 原理
原子累加器要比原子整数做相同的累加类操作时快很多,原因在下方解析:
优化机制
LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能
CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程空循环,自旋转)
优化核心思想:数据分离,将 AtomicLong 的单点的更新压力分担到各个节点,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能
分段 CAS 机制:
- 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上
- 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总
- 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能
自动分段迁移机制:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作
伪共享
Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的
Cell 是数组形式,在内存中是连续存储的,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,从而导致对方的数据失效,需要重新去主存获取,影响效率

@sun.misc.Contended:防止缓存行伪共享,在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效

源码分析
Striped64 类成员属性:
工作流程:
- cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域
- 在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上
- 分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改
- 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,数组长度总是 2 的 n 次幂,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8
方法分析:
- LongAdder#add:累加方法
- Striped64#longAccumulate:cell 数组创建
sum:获取最终结果通过 sum 整合,保证最终一致性,不保证强一致性
4、UnSafe
Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问
Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针
模拟实现原子整数:
5、不可变
不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象
不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式
String 类也是不可变的,该类和类中所有属性都是 final 的
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
- 无写入方法(set)确保外部不能对内部属性进行修改
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过创建副本对象来避免共享的方式称之为保护性拷贝
6、final 原理
源码
字节码
final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
写屏障作用:
- 数据同步到主存,保证可见性
- 防止屏障上方代码重排到下方
其他线程访问 final 修饰的变量:
- 数值较小会复制一份放入栈中直接访问,指令为bipush,效率高
- 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取,指令为ldc
- 不使用 final 修饰,指定为 getstatic ,需要从共享内存获取,效率低
7、享元模式
享元模式(Flyweight pattern): 用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式
异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池
工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象
基于该模式的自定义连接池实现:
8、无状态
无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量
Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
五、线程池
1、自定义线程池
阻塞队列实现
- 实现阻塞获取方法 task()
- 实现带超时阻塞获取方法 poll()
- 实现阻塞插入方法 put()
- 实现带超时阻塞插入方法 offer()
线程池ThreadPool实现
- 线程池持有任务队列、线程集合、核心线程数、超时时间、时间单位
- 由于线程池的线程要实现复用,因此使用Worker将其包装一层
- worker 线程会在持有的任务Runnable task 不为空或者线程池持有的任务队列不为空时持续执行任务
- 当二者都为空时,如果使用阻塞的 task() 方法会一直阻塞等待队列直到有元素出现,也可以替换为 poll() 方法超时后退出循环
- 当退出循环时候将当前worker 线程释放,即从workers集合移除即可
- 由于worker 集合为hashSet,因此需要加锁处理来保证线程安全性
- 实现线程池的执行 execute 方法
- 当workers集合的线程数小于核心线程数时,可以新创建worker线程,加入集合,随后启动本线程
- 否则,即待执行任务数超过已存在的核心线程数时,将任务线程加入任务队列进行等待。
拒绝策略
当任务数超过核心线程数时,需要给用户一个选择来决定超量的任务该如何处理,因此这里可以使用策略模式来实现这个逻辑。
新增拒绝策略接口:
线程池ThreadPool新增拒绝策略的成员变量,同时加入构造方法中:
修改ThreadPool的execute方法支持使用用户提供的策略:
阻塞队列新增 tryPut 方法:
- 若队列容量已满,使用拒绝策略处理
- 否则,加入任务队列,唤醒消费者来消费即可
测试
由调用者实现几种拒绝策略:
2、ThreadPoolExecutor

线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值:

四种状态:
状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 |
RUNNING | 111 | Y | Y | ㅤ |
SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 |
STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 |
TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
TERMINATED | 011 | - | - | 终止状态 |
线程池创建
存放线程的容器:
构造方法:
参数介绍:
- corePoolSize:核心线程数,定义了最小可以同时运行的线程数量
- maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数
- keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于
corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到keepAliveTime时间超过销毁
- unit:
keepAliveTime参数的时间单位
- workQueue:阻塞队列,存放被提交但尚未被执行的任务
- threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字
- handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略
- AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略
- CallerRunsPolicy:让调用者运行的调节机制,将某些任务回退到调用者,从而降低新任务的流量
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常
- DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务
- Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题
- Netty:创建一个新线程来执行任务
- ActiveMQ:带超时等待(60s)尝试放入队列
- PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
RejectedExecutionHandler 下有 4 个实现类:
补充:其他框架拒绝策略
工作原理
- 创建线程池,这时没有创建线程(懒惰),等待提交过来的任务请求,调用 execute 方法才会创建线程
- 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务,对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,会从队列中取下一个任务来执行
- 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小
3、Executors 工具类
Executors
Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool
- newFixedThreadPool:创建一个拥有 n 个线程的线程池
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- LinkedBlockingQueue 是一个单向链表实现的阻塞队列,默认大小为
Integer.MAX_VALUE,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) - 适用于任务量已知,相对耗时的长期任务
- newCachedThreadPool:创建一个可扩容的线程池
- 核心线程数是 0, 最大线程数是 29 个 1,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 OOM
- SynchronousQueue 作为阻塞队列,没有容量,对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止(类似一手交钱、一手交货)
- 适合任务数比较密集,但每个任务执行时间较短的情况
- newSingleThreadExecutor:创建一个只有 1 个线程的单线程池
- 保证所有任务按照指定顺序执行,线程数固定为 1,任务数多于 1 时会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放
对比:
- 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,线程池会新建一个线程,保证池的正常工作
- Executors.newSingleThreadExecutor() 线程个数始终为 1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法
- Executors.newFixedThreadPool(1) 初始时为 1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
开发要求
阿里巴巴 Java 开发手册要求:
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题
- 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险
- FixedThreadPool 和 SingleThreadPool:请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
- CacheThreadPool 和 ScheduledThreadPool:允许创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM
Executors 返回的线程池对象弊端如下:
创建多大容量的线程池合适?
- 一般来说池中总线程数是核心池线程数量两倍,确保当核心池有线程停止时,核心池外有线程进入核心池
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存
上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换
核心线程数常用公式:
- CPU 密集型任务 (N+1): 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析
- I/O 密集型任务: 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数),阻塞系数在 0.8~0.9 之间
IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
4、ExecutorService 接口
提交方法
ExecutorService 类 API:
方法 | 说明 |
void execute(Runnable command) | 执行任务(Executor 类 API) |
Future<?> submit(Runnable task) | 提交任务 task() |
Future submit(Callable<T> task) | 提交任务 task,用返回值 Future 获得任务执行结果 |
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) | 提交 tasks 中所有任务 |
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 |
T invokeAny(Collection<? extends Callable<T>> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 |
execute 和 submit 都属于线程池的方法,对比:
- execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask,然后调用 execute 执行
- execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出
关闭方法
ExecutorService 类 API:
方法 | 说明 |
void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) |
List<Runnable> shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 |
boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true |
boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true |
boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 |
处理异常
execute 会直接抛出任务执行时的异常,submit 会吞掉异常,有两种处理方法
方法 1:主动捉异常
方法 2:使用 Future 对象
- future.get() 会抛出异常
5、Timer
Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务
- 需使用 ScheduledThreadPoolExecutor 优化
6、ScheduledThreadPoolExecutor
任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor:
- 使用内部类 ScheduledFutureTask 封装任务
- 使用内部类 DelayedWorkQueue 作为线程池队列
- 重写 onShutdown 方法去处理 shutdown 后的任务
- 提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法,以便开发者进行扩展
构造方法:
Executors.newScheduledThreadPool(int corePoolSize)常用 API:
ScheduledFuture<?> schedule(Runnable/Callable<V>, long delay, TimeUnit u):延迟执行任务
ScheduledFuture<?> scheduleAtFixedRate(Runnable/Callable<V>, long initialDelay, long period, TimeUnit unit):定时执行周期任务,参数为初始延迟时间、间隔时间、单位- 执行时间为 Max(上一个任务执行耗时,period)
ScheduledFuture<?> scheduleWithFixedDelay(Runnable/Callable<V>, long initialDelay, long delay, TimeUnit unit):定时执行周期任务,参数为初始延迟时间、间隔时间、单位- 执行时间为上一个任务end之后延期delay后执行
与Timer不同若一个线程抛出异常,不会影响其他线程。
成员属性
成员变量
- shutdown 后是否继续执行周期任务:
- shutdown 后是否继续执行延迟任务:
- 取消方法是否将该任务从队列中移除:
- 任务的序列号,可以用来比较优先级:
延迟任务
ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对延时执行、周期执行的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列
在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask
成员变量:
- 任务序列号:
- 执行时间:
fixed-rate:两次开始启动的间隔,fixed-delay:一次执行结束到下一次开始启动
- 实际的任务对象:
- 任务在队列数组中的索引下标:
成员方法:
- 构造方法:
- compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序
- run():执行任务,非周期任务直接完成直接结束,周期任务执行完后会设置下一次的执行时间,重新放入线程池的阻塞队列,如果线程池中的线程数量少于核心线程,就会添加 Worker 开启新线程
周期任务正常完成后任务的状态不会变化,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行
- reExecutePeriodic():准备任务的下一次执行,重新放入阻塞任务队列
- cancel():取消任务
7、Tomcat 线程池

- LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor 只负责【接收新的 socket 连接】
- Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
- 如果总线程数达到 maximumPoolSize
- 这时不会立刻抛 RejectedExecutionException 异常
- 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
源码 tomcat-7.0.42
Connector 配置
配置项 | 默认值 | 说明 |
acceptorThreadCount | 1 | acceptor 线程数量 |
pollerThreadCount | 1 | poller 线程数量 |
minSpareThreads | 10 | 核心线程数,即 corePoolSize |
maxThreads | 200 | 最大线程数,即 maximumPoolSize |
executor | - | Executor 名称,用来引用下面的 Executor |
Executor 线程配置
配置项 | 默认值 | 说明 |
threadPriority | 5 | 线程优先级 |
daemon | true | 是否守护线程 |
minSpareThreads | 25 | 核心线程数,即 corePoolSize |
maxThreads | 200 | 最大线程数,即 maximumPoolSize |
maxIdleTime | 60000 | 线程生存时间,单位是毫秒,默认值即 1 分钟 |
maxQueueSize | Integer.MAX_VALUE | 队列长度 |
prestartminSpareThreads | false | 核心线程是否在服务器启动时启动 |
与Java的线程池不太一样,Tomcat当任务数超过核心线程数小于最大线程数时不会直接加入队列,而是创建救急线程:

8、ForkJoin
Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 CPU 密集型运算,用于并行计算
任务拆分:将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解
- Fork/Join 在分治的基础上加入了多线程,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率
- ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 CPU 核心数大小相同的线程池
- 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率:
- 每个线程都维护了一个双端队列,用来存储需要执行的任务
- 工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行
- 窃取的必须是最晚的任务,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争
利用多线程是吸纳累加例子:
9、Future & FutureTask
线程使用
FutureTask 未来任务对象,继承 Runnable、Future 接口,用于包装 Callable 对象,实现任务的提交
构造方法:
成员属性
FutureTask 类的成员属性:
- 任务状态:
- 任务对象:
- 存储任务执行的结果,这是 run 方法返回值是 void 也可以获取到执行结果的原因:
- 执行当前任务的线程对象:
- 线程阻塞队列的头节点:
- 内部类:
成员方法
FutureTask 类的成员方法:
- FutureTask#run:任务执行入口
FutureTask#set:设置正常返回值,首先将任务状态设置为 COMPLETING 状态代表完成中,逻辑执行完设置为 NORMAL 状态代表任务正常执行完成,最后唤醒 get() 阻塞线程
FutureTask#setException:设置异常返回值
FutureTask#finishCompletion:唤醒 get() 阻塞线程
FutureTask#handlePossibleCancellationInterrupt:任务中断处理
- FutureTask#get:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,一般有多个线程 get,只有一个线程 run
FutureTask#awaitDone:get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待
FutureTask#report:封装运行结果,可以获取 run() 方法中设置的成员变量 outcome,这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因
- FutureTask#cancel:任务取消,打断正在执行该任务的线程
六、同步器
1、AQS
核心思想
AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器
AQS 用状态属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- 独占模式是只有一个线程能够访问资源,如 ReentrantLock
- 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式
AQS 核心思想:
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态
- 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中
CLH 是一种基于单向链表的高性能、公平的自旋锁,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码
- 使用者继承
AbstractQueuedSynchronizer并重写指定的方法
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
- 默认情况下,每个方法都抛出
UnsupportedOperationException
- 这些方法的实现必须是内部线程安全的
- AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用
自定义不可重入锁
基于 AQS 实现不可抽个很难入的独占锁
测试类
基本原理
AbstractQueuedSynchronizer 中 state 设计:
- state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态
- state 使用 volatile 修饰配合 cas 保证其修改时的原子性
- state 表示线程重入的次数(独占模式)或者剩余许可数(共享模式)
- state API:
protected final int getState():获取 state 状态protected final void setState(int newState):设置 state 状态protected final boolean compareAndSetState(int expect,int update):CAS 安全设置 state
封装线程的 Node 节点中 waitstate 设计:
- 使用 volatile 修饰配合 CAS 保证其修改时的原子性
- 表示 Node 节点的状态,有以下几种状态:
阻塞恢复设计:
- 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程可以通过 interrupt 打断
队列设计:
- 使用了 FIFO 先入先出队列,并不支持优先级队列,同步队列是双向链表,便于出队入队
2、ReentrantLock
与 synchronized 对比
ReentrantLock 相对于 synchronized 具备如下特点:
- 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
- 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同
- 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
- 可中断:ReentrantLock 可中断,而 synchronized 不行
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
- 不公平锁的含义是阻塞队列内公平,队列外非公平
- 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列
- ReentrantLock 可以设置超时时间,synchronized 会一直等待
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 条件变量
- 两者都是可重入锁
基本语法
ReentrantLock 类 API:
public void lock():获得锁- 如果锁没有被另一个线程占用,则将锁定计数设置为 1
- 如果当前线程已经保持锁定,则保持计数增加 1
- 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态
public void unlock():尝试释放锁- 如果当前线程是该锁的持有者,则保持计数递减
- 如果保持计数现在为零,则锁定被释放
- 如果当前线程不是该锁的持有者,则抛出异常
可重入
可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁。需要保证加解锁次数一直,哦负责抛出异常。
源码解析参考:
nonfairTryAcquire(int acquires)) 和 tryRelease(int releases)可打断
public void lockInterruptibly():获得可打断的锁- 如果没有竞争此方法就会获取 lock 对象锁
- 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断
原理-不可打断
不可打断模式:即使它被打断,仍会驻留在 AQS 阻塞队列中,一直要等到获得锁后才能得知自己被打断了
- 被打断后只是保存了一个标记,只有在 acquireQueued 方法里进入第一个if后(即获得到锁)之后才会返回被打断的标记
- 然后触发一次打断效果(线程是运行状态,因此只会重置打断标记)
原理-可打断
- 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly,被打断后会直接抛出异常
锁超时
public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出注意:tryLock 期间也可以被打断
tryLock 解决哲学家就餐问题
原理:拿到一根拿不到另一根就放掉这根即可。
公平锁
说明:公平锁一般没有必要,会降低并发度
原理-非公平锁
1、加锁
NonfairSync 继承自 AQS
- 没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1
- 第一个竞争出现:Thread-1 执行,CAS 尝试将 state 由 0 改为 1,结果失败(第一次),进入 acquire 逻辑

- 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次)
- 当前 AQS 处于无锁状态(不检查aqs队列体现非公平性)
- 加锁线程就是当前线程,说明发生了锁重入
- 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

- 线程节点创建Node成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程
- acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞
- 如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次)
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)
- 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
- 再有多个线程经历竞争失败后:

2、解锁
ReentrantLock#unlock:释放锁
Thread-0 释放锁,进入 release 流程
- 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0
- 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor
- 进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点
- 找到队列中距离 head 最近的一个没取消的 Node,unpark 恢复其运行,本例中即为 Thread-1
- 回到 Thread-1 的 acquireQueued 流程
从后向前的唤醒的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失尾节点
- 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 会清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收(图中有错误,原来的头节点的 waitStatus 被改为 0 了)

- 如果这时有其它线程来竞争(非公平),例如这时有 Thread-4 来了并抢占了锁
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

原理-公平锁
与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去 CAS 竞争
条件变量
synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量
ReentrantLock 类获取 Condition 对象:
public Condition newCondition()Condition 类 API:
void await():当前线程从运行状态进入等待状态,释放锁
void signal():唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁
使用流程:
- await / signal 前需要获得锁
- await 执行后,会释放锁进入 ConditionObject 等待
- await 的线程被唤醒去重新竞争 lock 锁
- 线程在条件队列被打断会抛出中断异常
- 竞争 lock 锁成功后,从 await 后继续执行
原理-条件变量
await
总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,每个 Condition 对象都包含一个等待队列
- 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里不存在并发

- 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
- 接下来 Thread-0 进入 AQS 的 fullyRelease 流程,释放同步器上的锁
- fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功

- Thread-0 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列,没有就 park 阻塞 Thread-0
- await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断:在条件队列被打断的线程需要抛出异常
- 最后开始处理中断状态:
signal
- 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内线程安全
- 执行 transferForSignal,先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部,将 Thread-3 的 waitStatus 改为 -1

- Thread-1 释放锁,进入 unlock 流程
3、ReentrantReadWriteLock
基本使用
独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁
共享锁:指该锁可以被多个线程锁持有
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁
相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁
- 读-读能共存、读-写不能共存、写-写不能共存
- 读锁不支持条件变量
- 构造方法:默认是非公平锁,可以指定参数创建公平锁
- 重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写
- 重入时降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁
缓存应用
缓存更新时,是先清缓存还是先更新数据库
- 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了旧数据
- 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据
原理-加锁
- t1 线程:w.lock(写锁),成功上锁 state = 0_1
- t2 r.lock(读锁),进入 tryAcquireShared 流程:
- 返回 -1 表示失败
- 如果返回 0 表示成功
- 返回正数表示还有多少后继节点支持共享模式,读写锁返回 1
- 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park

- 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子

原理-解锁
- t1 w.unlock, 写锁解锁
- 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一
- 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,唤醒连续的所有的共享节点

- 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
- t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点
- t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束

4、StampedLock
StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能
特点:
- 在使用读锁、写锁时都必须配合戳使用
- StampedLock 不支持条件变量
- StampedLock 不支持重入
基本用法
- 加解读锁:
- 加解写锁:
- 乐观读,StampedLock 支持
tryOptimisticRead()方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性
提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法:
- 读-读可以优化
- 读-写优化读,补加读锁
5、Semaphore
基本使用
synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行
Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁
构造方法:
public Semaphore(int permits):permits 表示许可线程的数量(state)
public Semaphore(int permits, boolean fair):fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程
常用API:
public void acquire():表示获取许可
public void release():表示释放许可,acquire() 和 release() 方法之间的代码为同步代码
原理-加解锁
加锁流程:
- Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞

解锁流程:
- 这时 Thread-4 释放了 permits,状态如下

- 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
6、CountDownLatch
基本使用
CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成
构造器:
public CountDownLatch(int count):初始化唤醒需要的 down 几步
常用API:
public void await():让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待
public void countDown():计数器进行减 1(down 1)
原理:基本的AQS原理,state 初始值就是许可数
应用:同步等待多个 Rest 远程调用结束
7、CyclicBarrier
基本使用
CyclicBarrier:可循环使用的CounDownLatch,循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行
常用方法:
public CyclicBarrier(int parties, Runnable barrierAction):用于在线程到达屏障 parties 时,执行 barrierAction- parties:代表多少个线程到达屏障开始触发线程任务
- barrierAction:线程任务
public int await():线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障
原理:基本的AQS原理,增加了重置state操作,state 减到0后,唤醒所有阻塞的await线程并重置state为初始值
- 基于 ReentrantLock 和 Condition 条件队列
与 CountDownLatch 的区别:CyclicBarrier 是可以重用的
应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发

七、线程安全集合
三类:
- Blocking Queue
- CopyOnWrite List
- Concurrent Map
1、ConcurrentHashMap
集合对比
- Hashtable 和 HashMap(JDK7) 底层是数组 + 链表,JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树
- ConcurrentHashMap、Hashtable 不允许 null key 和 null value, HashMap 允许 null key 和 value
- ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1
- HashMap JDK 1.7 是头插法,会产生并发死链问题,JDK 8 是尾插法,没有并发死链问题,但仍然会有其他问题(例如扩容丢失数据)
- 并发死链:头插法会导致链表在多线程下的迁移指针产生错乱,导致链表成环:A→B→A
总体步骤
- 初始化,cas 懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索
- 扩容迁移完毕会将 bin 头节点标记为 ForwardingNode
- 若已被标记则去新数组查找,否则直接在旧数组查找
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加(类似 LongAdder 原理来提高累加效率)
JDK7 原理
ConcurrentHashMap 对锁粒度进行了优化,分段锁技术,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。
底层结构: Segment 数组 + HashEntry 数组 + 链表(数组 + 链表是 HashMap 的结构)
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 JDK8 中是类似的
- 缺点:并发度固定,Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

基本信息
变量
- 存储数组:
- 散列表的长度:
- 负载因子,JDK1.8 的 ConcurrentHashMap 中是固定值:
- 阈值:
- 节点哈希值:
- 扩容过程:volatile 修饰保证多线程的可见性
- 累加统计:
- 控制变量:
- 1 表示当前 table 正在初始化(有线程在创建 table 数组),当前线程需要自旋等待
- 其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + 1
- 如果 table 未初始化,表示初始化大小
- 如果 table 已经初始化,表示下次扩容时的触发条件(阈值,元素个数,不是数组的长度)
sizeCtl < 0:
sizeCtl = 0,表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小
sizeCtl > 0:
构造方法
- 无参构造, 散列表结构延迟初始化,默认的数组大小是 16:
- 多个参数构造方法:
PUT
- putVal()
- spread():扰动函数
将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,与树化节点和转移节点区分,把高低位都利用起来减少哈希冲突,保证散列的均匀性
- initTable():初始化数组,延迟初始化
- treeifyBin():树化方法
- addCount():添加计数,代表哈希表中的数据总量
扩容方法
扩容机制:
- 当链表中元素个数超过 8 个,数组的大小还未超过 64 时,此时进行数组的扩容,如果超过则将链表转化成红黑树
- put 数据后调用 addCount() 方法,判断当前哈希表的容量超过阈值 sizeCtl,超过进行扩容
- 增删改线程发现其他线程正在扩容,帮其扩容
常见方法:
- transfer():数据转移到新表中,完成扩容
链表处理的 LastRun 机制,可以减少节点的创建

- helpTransfer():帮助扩容机制
GET
ConcurrentHashMap 使用 get() 方法获取指定 key 的数据
- get():获取指定数据的方法
- ForwardingNode#find:转移节点的查找方法
删除方法
- remove():删除指定元素
- replaceNode():替代指定的元素,会协助扩容,增删改(写)都会协助扩容,查询(读)操作不会,因为读操作不涉及加锁
2、LinkedBlockingDeque
入队出队
加锁分析
高明之处在于用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析:
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
put 操作:
take 操作:
性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
3、CopyOnWriteArrayList
原理分析
CopyOnWriteArrayList 采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的并发读,读写分离
CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式
- 存储结构:
- 全局锁:保证线程的执行安全
- 新增数据:需要加锁,创建新的数组操作
- 读操作:不加锁,在原数组上操作
适合读多写少的应用场景
- 迭代器:CopyOnWriteArrayList 在返回迭代器时,创建一个内部数组当前的快照(引用),即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见
弱一致性
数据一致性就是读到最新更新的数据:
- 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值
- 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到
安全失败
在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败
- 快速失败:在 A 线程使用迭代器对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常
- AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化
- 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常
- 安全失败:采用安全失败机制的集合容器,在迭代器遍历时直接在原集合数组内容上访问,但其他线程的增删改都会新建数组进行修改,就算修改了集合底层的数组容器,迭代器依然引用着以前的数组(快照思想),所以不会出现异常
4、ConcurrentLinkedDeque
并发编程中,需要用到安全的队列,实现安全队列可以使用 2 种方式:
- 加锁,这种实现方式是阻塞队列
- 使用循环 CAS 算法实现,这种方式是非阻塞队列
ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素
补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列
ConcurrentLinkedQueue 使用约定:
- 不允许 null 入列
- 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到
- 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点
- head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性
入队
与传统的链表不同,单线程入队的工作流程:
- 将入队节点设置成当前队列尾节点的下一个节点
- 更新 tail 节点,如果 tail 节点的 next 节点不为空,则将入队节点设置成 tail 节点;如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,存在滞后性
当 tail 节点和尾节点的距离大于等于 1 时(每入队两次)更新 tail,可以减少 CAS 更新 tail 节点的次数,提高入队效率
线程安全问题:
- 线程 1 线程 2 同时入队,无论从哪个位置开始并发入队,都可以循环 CAS,直到入队成功,线程安全
- 线程 1 遍历,线程 2 入队,所以造成 ConcurrentLinkedQueue 的 size 是变化,需要加锁保证安全
- 线程 1 线程 2 同时出列,线程也是安全的
出队
出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新 head 节点
- 当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点
- 当 head 节点里没有元素时,出队操作才会更新 head 节点
批处理方式可以减少使用 CAS 更新 head 节点的消耗,从而提高出队效率
size():用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确
- 作者:ITNXD
- 链接:https://blog.itnxd.eu.org/article/juc
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

