
从本章开始,我们正式进入 Java 多线程系列的学习,透彻理解 Java 并发编程。内容很多建议收藏慢慢看......
本篇主要内容如下:
多线程这块知识的学习,真正的难点不在于多线程程序的逻辑有多复杂,而在于理清 J.U.C 包中各个多线程工具类之间的关系、特点及其使用场景(从整体到局部、高屋建瓴,这对学习任何知识都至关重要)。
Chaya:彻底掌握必须深入源码级别了解底层细节吗?
真正掌握 Java 多线程,必须要弄懂 J.U.C,并不是说必须是源码级别的,这里有几个关键点需要注意下。
在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会 面临非常多的挑战。
多线程难在哪里?
单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程
多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点
单核处理器也支持多线程执行代码吗?
是的,CPU 通过给每个线程分配 CPU 时间片来实现 这个机制。时间片是 CPU 分配给各个线程的时间,因为时间片非常短。
所以 CPU 通过不停地切 换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是 便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第 多少行,等查完单词之后,能够继续读这本书。
线程上下文切换是有成本的,主要体现在以下几个方面:
如下图所示,保存上下文和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。

第一步:用 jstack 命令 dump 线程信息,看看 pid 为 3117 的进程里的线程都在做什么。
sudo -u admin /opt/magebyte/java/bin/jstack 31177 > /home/magebyte/dump17
第二步:统计所有线程分别处于什么状态,发现 300 多个线程处于 WAITING(onobjectmonitor)状态。
grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
第三步:打开 dump 文件查看处于 WAITING(onobjectmonitor)的线程在做什么。发现这些线 程基本全是 Tomcat 的工作线程,在 await。
说明 JBOSS 线程池里线程接收到的任务太少,大量线 程都闲着。
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
- locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
第四步:减少 Tomcat 的工作线程数,找到 Tomcat 的线程池配置信息,将 maxThreads 降到 200。
# 最大工作线程数,默认200。
server.tomcat.max-threads=200
在这里给大家分享一个生产级别 Tomcat 配置推荐。
server:
port:9000
tomcat:
uri-encoding:UTF-8
max-threads:800#最大工作线程数量
min-spare-threads:20#最小工作线程数量
max-connections:10000#一瞬间最大支持的并发的连接数
accept-count:200#等待队列长度
参数解释
使用多线程提高性能,在并发读写共享资源的时候,不恰当的使用会导致死锁问题。一旦产生死锁,就会造成系统功能不可 用。
在 Java 中,死锁(Deadlock)情况是指:两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。
线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。

如上图所示,Thread-1持有资源Object1但是需要资源Object2完成自身任务,同样的,Thread-2持有资源Object2但需要Object1,双方都在等待对方手中的资源但都不释放自己手中的资源,从而进入死锁。
如下死锁代码:
public class DeadLockExample {
public Object resourceA = new Object();
public Object resourceB = new Object();
public static void main(String[] args) {
DeadLockExample deadLockExample = new DeadLockExample();
Runnable runnableA = new Runnable() {
@Override
public void run() {
synchronized(deadLockExample.resourceA) {
System.out.printf(
"[INFO]: %s get resourceA" + System.lineSeparator(),
Thread.currentThread().getName()
);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf(
"[INFO]: %s trying to get resourceB" + System.lineSeparator(),
Thread.currentThread().getName()
);
synchronized(deadLockExample.resourceB) {
System.out.printf(
"[INFO]: %s get resourceB" + System.lineSeparator(),
Thread.currentThread().getName()
);
}
System.out.printf(
"[INFO]: %s has done" + System.lineSeparator(),
Thread.currentThread().getName()
);
}
}
};
Runnable runnableB = new Runnable() {
@Override
public void run() {
synchronized(deadLockExample.resourceB) {
System.out.printf(
"[INFO]: %s get resourceB" + System.lineSeparator(),
Thread.currentThread().getName()
);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf(
"[INFO]: %s trying to get resourceA" + System.lineSeparator(),
Thread.currentThread().getName()
);
synchronized(deadLockExample.resourceA) {
System.out.printf(
"[INFO]: %s get resourceA" + System.lineSeparator(),
Thread.currentThread().getName()
);
}
System.out.printf(
"[INFO]: %s has done" + System.lineSeparator(),
Thread.currentThread().getName()
);
}
}
};
new Thread(runnableA).start();
new Thread(runnableB).start();
}
}
程序输出:
[INFO]: Thread-0 get resourceA
[INFO]: Thread-1 get resourceB
[INFO]: Thread-0 trying to get resourceB
[INFO]: Thread-1 trying to get resourceA
JDK自带了一些简单好用的工具,可以帮助我们检测死锁(如:jstack)。使用jstack侦测目标 JVM 进程.
$ jstack $(jps -l | grep 'DeadLockExample' | cut -f1 -d ' ')
输出如下。
...
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadLockExample$2.run(DeadLockExample.java:58)
- waiting to lock <0x000000076ab660a0> (a java.lang.Object)
- locked <0x000000076ab660b0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at DeadLockExample$1.run(DeadLockExample.java:28)
- waiting to lock <0x000000076ab660b0> (a java.lang.Object)
- locked <0x000000076ab660a0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
什么是资源限制?
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
例如,服务器的带宽只有 2Mb/s,某个资源的下载速度是 1Mb/s 每秒,系统启动 10 个线程下载资 源,下载速度不会变成 10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。
硬件资源限 制有带宽的上传/下载速度、硬盘读写速度和 CPU 的处理速度。
软件资源限制有数据库的连接数和 socket 连接数等。
资源限制会引发什么问题?
是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不 会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
例如,之前看到一段程 序使用多线程在办公网并发地下载和处理数据时,导致 CPU 利用率达到 100%,几个小时都不 能运行完成任务,后来修改成单线程,一个小时就执行完成了。
如何在资源限制的情况下进行并发编程呢?
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个 Java 程序,操作 系统就会创建一个 Java 进程。
现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局 部变量等属性,并且能够访问共享的内存变量。
处理器在这些线程上高速切换,让使用者感觉 到这些线程在同时执行。

在 Java 程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。
一旦run()方法执行完毕,线程就结束了。因此,Java 线程的状态有以下几种:
run()方法的 Java 代码;sleep()方法正在计时等待;run()方法执行完毕。下图源自《Java 并发编程艺术》图 4-1

start()方法之后,线程处于就绪状态,就绪意味着该线程可以执行,但具体啥时候执行将取决于 JVM 里线程调度器的调度。WAITING,它可以在指定的时间自行返回。线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执 行,直到终止。
Chaya:如何让线程间实现通信,让多个线程能够相互配合完成工作?
java 线程之间的通信方式总共有 8 种,分别是 volatile、synchronized、interrupt、wait、notify、notifyAll、join、管道输入/输出。
Java 支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个 变量的副本(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份副本,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特 性)

线程会将内存中的数据,拷贝到各自的本地内存中( 这里的本地内存指的是 cpu cache ( 比如 CPU 的一级缓存、二级缓存等 ),寄存器)。
当某个变量被 volatile 修饰并且发生改变时,volatile 变量底层会通过 lock 前缀的指令,将该变量写会主存,同时利用 缓存一致性协议,促使其他线程的本地变量的数据无效,从而再次直接从主存读取数据。
代码案例,通过 标志位 来终止线程。
private staticclass Runner implements Runnable{
privatelong i;
privatevolatileboolean running =true;
@Override
public void run() {
System.out.println("current Thread Name:"+Thread.currentThread().getName());
while (running ){
i++;
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
}
}
System.out.println("Count i= "+i);
System.out.println("current Thread Name:"+Thread.currentThread().getName());
}
public void cancel(){
running =false;
System.out.println("running=false");
}
}
关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程 在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性 和排他性。
同步就好像在公司上班,厕所只有一个,现在一帮人同时想去「带薪拉屎」占用厕所,为了保证厕所同一时刻只能一个员工使用,通过排队互斥实现。
synchronized 的实现原理是对一 个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个 线程获取到由 synchronized 所保护对象的监视器。
监视器锁(Monitor 另一个名字叫管程)本质是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。

在 Java 虚拟机 (HotSpot) 中,Monitor 是基于 C++ 实现的,由 ObjectMonitor 实现的, 几个关键属性:
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1。
若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
Java 多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。
等待/通知机制,是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()方法返回,进而 执行后续操作。上。

public class TestSync {
public static void main(String[] args) {
// 定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();// 唤醒B线程
}
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
等待方遵循如下原则。
对应的伪代码如下。
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
满足条件对应的处理逻辑
}
通知方规范
通知方遵循如下原则。
对应的伪代码如下。
synchronized(对象) {
改变条件
对象.notifyAll();
}
ThreadLocal 是 Java 并发包(java.lang)中的一个类,用于为每个线程创建独立的变量副本,实现线程间的数据隔离。
它通过空间换时间的方式,避免多线程共享变量时的同步开销,适用于需要线程私有数据的场景。

在 Web 应用中通过 ThreadLocal 传递用户身份信息是典型的生产级场景。
public class UserContext {
// 使用静态内部类实现懒加载,保证线程安全
privatestaticfinal ThreadLocal<UserInfo> currentUser = new ThreadLocal<>();
public static void set(UserInfo user) {
currentUser.set(user);
}
public static UserInfo get() {
UserInfo user = currentUser.get();
if (user == null) {
thrownew IllegalStateException("User not found in current thread context");
}
return user;
}
public static void clear() {
currentUser.remove(); // 必须显式清理防止内存泄漏
}
}
@Component
publicclass AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtTokenService jwtTokenService; // 自定义的JWT解析服务
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取Token
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
UserInfo user = jwtTokenService.parseToken(token); // 解析用户信息
UserContext.set(user); // 存储到ThreadLocal
}
returntrue;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear(); // 请求结束时必须清理ThreadLocal
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截API请求
.excludePathPatterns("/api/public/**"); // 排除公共接口
}
}
@RestController
@RequestMapping("/api")
public class OrderController {
@GetMapping("/orders")
public ResponseEntity<List<Order>> getOrders() {
UserInfo currentUser = UserContext.get(); // 无需参数传递
return orderService.findByUserId(currentUser.getUserId());
}
}
Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁。
需要注意的是类锁其实也是对象锁。
Java 类只有一个 Class 对象(可以有多个实例对象,多个实例共享这个 Class 对象),而 Class 对象也是特殊的 Java 对象。所以我们常说的类锁,其实就是 Class 对象的锁。
我们通常使用synchronized关键字来给一段代码或一个方法上锁。它通常有以下三种形式。
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
这里介绍一下“临界区”的概念。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。
monitor)便是对象实例(this)monitor)便是对象的Class实例(每个对象只有一个Class实例)monitor)是指定对象实例底层实现原理是通过monitorenter与monitorexit指令(获取锁、释放锁)。
monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,J V M需要保证每一个 monitorenter都有monitorexit与之对应。
任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。
monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者monitor,重新进入,则monitor的进入数加1monitorexit,monitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexitmonitor,则该线程进入阻塞状态,直到monitor的进入数为 0,再重新尝试获取monitor的所有权
Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在 Java 6 以前,所有的锁都是”重量级“锁。
所以在 Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
Synchronized 机制及锁膨胀流程详见:https://www.processon.com/view/62c005a10e3e746592070665
Java中每个对象都拥有对象头,对象头由Mark World 、指向类的指针、以及数组长度三部分组成,本文,我们只需要关心Mark World 即可,Mark World 记录了对象的HashCode、分代年龄和锁标志位信息。

Mark World 简化结构
锁状态 | 存储内容 | 锁标记 |
|---|---|---|
无锁 | 对象的 hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程 ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
读者们只需知道,锁的升级变化,体现在锁对象的对象头Mark World部分,也就是说Mark World的内容会随着锁升级而改变。
Java1.5以后为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,Synchronized的升级顺序是 「无锁-->偏向锁-->轻量级锁-->重量级锁,只会升级不会降级」
偏向锁是 JDK6 中的重要引进,因为 HotSpot 作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
线程执行同步代码或方法前,线程只需要判断对象头的Mark Word中线程ID与当前线程ID是否一致,如果一致直接执行同步代码或方法,具体流程如下。

引入偏向锁主要目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。
引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word。
此时线程堆栈与对象头的状态如下图所示:

若一个线程获得锁时发现是轻量级锁,它会将对象的 Mark Word 复制到栈帧中的锁记录 Lock Record 中(Displaced Mark Word 里面)。
线程尝试利用 CAS 操作将对象的 Mark Word 更新为指向 Lock Record 的指针,如果成功表示当前线程竞争到锁,则将锁标志位变成 00,执行同步操作;

img
如果失败,表示 MarkWord 已经被替换成了其他线程的锁记录,说明在与其他线程抢占竞争锁,当前线程就尝试使用自旋来获取锁,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁。
轻量级锁解锁和升级重量级锁
轻量级锁的释放也是通过 CAS 操作来进行的,当前线程使用 CAS 操作将 Displaced Mark Word 的内存复制回锁对象的 MarkWord 中,如果 CAS 操作替换成功,则说明释放锁成功;
如果 CAS 自旋多次还是替换失败的话,说明有其他线程尝试获取该锁,则需要将轻量级锁膨胀升级为重量级锁;
轻量级锁升级为重量级锁的流程

在 Java1.5 版本以前,我们开发多线程程序只能通过关键字 synchronized 进行共享资源的同步、临界值的控制。
随着版本的不断升级,JDK 对 synchronized 关键字的性能优化工作一直在继续,但是 synchronized 在使用的过程中还是存在着比较多的缺陷和不足,因此在 1.5 版本以后 JDK 增加了对显式锁的支持。
锁 Lock 除了能够完成关键字 synchronized 的语义和功能之外,它还提供了很多灵活方便的方法:
Lock 是一个接口,它定义了锁获取和释放的基本操作。
AbstractQueuedSynchronizer 以及常用 Lock 接口的实现 ReentrantLock。
Lock 接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。
Lock 的使用也很简单,代码如下。
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
在 finally 块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
注意:不要将获取锁的过程写在 try 块中,因为如果在获取锁(自定义锁的实现)时发生了异常,会导致锁无故释放。
队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获 取线程的排队工作。
我们可以理解 AQS 将整个加锁的算法逻辑进行封装,在加锁过程中,免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法getState()、setState(int newState)和compareAndSetState(int expect,int update)来进行操 作.
如果获取锁成功,直接扣减 AQS 的 State 值,不会涉及到 AQS。但如果当前线程获取锁失败,那么剩下的包括阻塞唤醒线程、重新发起获取锁之类的操作全都都会扔给 AQS 。
简单来说就是 AQS 包揽了同步机制的各种工作。
下图就是线程获取锁的大致流程:

这其实是一个模板方法模式来实现的,将加锁与解锁的变化与不变点隔离,不同类型的锁交给子类实现,同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
A Q S采用了模板方法设计模式,提供了两类模板,一类是独占式模板,另一类是共享形模式,对应的模板函数如下
acquire获取资源release释放资源acquireShared获取资源releaseShared释放资源下面就是使用 AQS 实现的最简单的独占锁,从代码也可以看出 AQS 大大降低了开发锁的难度:
class Mutex {
privatestaticclass Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
returntrue;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
privatefinal Sync sync = new Sync();
public void lock() {
sync.tryAcquire(1);
}
public void unlock() {
sync.tryRelease(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
AQS 的继承关系如下图所示:

AQS 继承了另外一个抽象类 AbstractOwnableSynchronizer,这个类的功能其实就是持有一个不能被序列化的属性 exclusiveOwnerThread ,它代表独占线程。
在属性中记录持有独占锁的线程的目的就是为了实现可重入功能,当下一次获取这个锁的线程与当前持有锁的线程相同时,就可以获取到锁,同时 AQS 的 state 值会加 1。

state同步状态Node组成的CLH队列ConditionObject条件变量(包含Node组成的条件单向队列),下面会分别对这三部分做介绍。在A Q S中维护了一个同步状态变量state,getState函数获取同步状态,setState、compareAndSetState函数修改同步状态。
对于A Q S来说,线程同步的关键是对state的操作,可以说获取、释放资源是否成功都是由state决定的,比如state>0代表可获取资源,否则无法获取。
所以state的具体语义由实现者去定义,现有的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch定义的state语义都不一样。
ReentrantLock的state用来表示是否有锁资源ReentrantReadWriteLock的state高16位代表读锁状态,低16位代表写锁状态Semaphore的state用来表示可用信号的个数CountDownLatch的state用来表示计数器的值Node 就是 AQS 实现各种队列的基本组成单元。它有以下几个属性:
AQS 总共有两种队列,一种是同步队列,代表的是正常获取锁释放锁的队列,一种是条件队列,代表的是每个 ConditionObject 对应的队列,这两种队列都是 FIFO 队列,也就是先进先出队列。
而同步队列的节点分为两种,一种是独占锁的节点,一种是共享锁的节点,它们唯一的区别就是 nextWaiter 这个指针的值。
如果是独占锁的节点,nextWaiter 的值是 null,如果是共享锁的节点,nextWaiter 会指向一个静态变量 SHARED 节点。
独占锁队列和共享锁队列如下图所示:

条件队列是单链,它没有空的头节点,每个节点都有对应的线程。条件队列头节点和尾节点的指针分别是 firstWaiter 和 lastWaiter ,如下图所示:

当某个线程执行了ConditionObject的await函数,阻塞当前线程,线程会被封装成Node节点添加到条件队列的末端,其他线程执行ConditionObject的signal函数,会将条件队列头部线程节点转移到C H L队列参与竞争资源,具体流程如下图。

线程获取资源失败,封装成Node节点从C L H队列尾部入队并阻塞线程,某线程释放资源时会把C L H队列首部Node节点关联的线程唤醒(此处的首部是指第二个节点),再次获取资源。

获取锁的模板方法如下,定义了整个加锁流程算法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 这里 Node.EXCLUSIVE 的值是 null
selfInterrupt();
}
tryAcquire(arg)方法需要具体的锁来实现的,这是模板方法这个方法主要是尝试获取锁,获取成功就不会再执行其他代码了,这个方法结束。
以非公平锁为例。

有获取资源,自然就少不了释放资源,AQS中提供了release模板方法来释放资源。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁,如果成功则唤醒后继节点的线程
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒CHL队列第二个线程节点
unparkSuccessor(h);
returntrue;
}
returnfalse;
}
release逻辑非常简单,流程图如下。


重入锁 ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实 现需要解决以下两个问题。
再说原理之前,先看下怎么使用。
private final Lock lock = new ReentrantLock();
public void foo() {
// 获取锁
lock.lock();
try{
// 程序执行逻辑
} finally{
// finally语句块可以确保lock被正确释放
lock.unlock();
}
}
我们将 lock()方法写在 try...finally 语句块中的目的是为了防止获取锁的过程中出现异常导致锁被意外。
先来看下整体结构。

ReentrantLock底层基于AbstractQueuedSynchronizer实现,它实现了Lock 接口,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync和FairSync是基于Sync扩展的子类,即ReentrantLock的非公平策略与公平策略,它们作为Lock接口功能的基本实现。
公平策略:在多个线程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁,这样按照“先来后到”的原则,对于每一个等待线程都是公平的。
非公平策略:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层 OS 调度)。
Sync 承了AbstractQueuedSynchronizer,是ReentrantLock的核心,后面的NonfairSync与FairSync都是基于Sync扩展出来的子类。
abstract staticclass Sync extends AbstractQueuedSynchronizer {
privatestaticfinallong serialVersionUID = -5179523762034025860L;
/**
* 非公平锁获取资源
*/
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
//获取当前状态
int c = getState();
// state==0 代表资源可获取
if (c == 0) {
//cas设置state为acquires,acquires传入的是1
if (compareAndSetState(0, acquires)) {
// cas成功,将当前线程设置持有锁的线程
setExclusiveOwnerThread(current);
// 返回成功
returntrue;
}
}
//如果state!=0,但是当前线程是持有锁线程,直接重入
elseif (current == getExclusiveOwnerThread()) {
//state状态+1
int nextc = c + acquires;
if (nextc < 0) // overflow
thrownew Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setState(nextc);
returntrue;
}
// 获取锁失败
returnfalse;
}
/**
* 释放资源
*/
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
//state状态-releases,releases传入的是1
int c = getState() - releases;
//如果当前线程不是持有锁线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
thrownew IllegalMonitorStateException();
boolean free = false;
//state-1后,如果c==0代表释放资源成功
if (c == 0) {
//返回状态设置为true
free = true;
//清空持有锁线程
setExclusiveOwnerThread(null);
}
//如果state-1后,state还是>0,
//代表当前线程有锁重入操作,需要做相应的释放次数,设置state值
setState(c);
return free;
}
....省略其他代码
}
Sync 默认提供了非公平锁的加锁方式,解锁方式tryRelease 非公平锁和公平锁都会用到,tryRelease 流程图就提前上菜。

NonfairSync就是非公平策略。在说非公平锁之前,回顾下 AQS 定义的获取锁算法流程。

线程释放锁时,会唤醒CLH队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH队列的线程参与竞争,所以非公平就体现在这里,非CLH队列线程与CLH队列线程竞争,各凭本事,不会因为你是CLH队列的线程,排了很久的队,就把锁让给你。
NonfairSync继承 Sync,并实现了 AQS 定义的 tryAcquire 方法。实现方式是 之前 Sync 提供的 nonfairTryAcquire 方法。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
对应的解锁资源则使用 Sync提供的 java.util.concurrent.locks.ReentrantLock.Sync#tryRelease方法。
nonfairTryAcquire(int acquires)方法,对于非公平锁,只要 CAS 设置 同步状态成功,则表示当前线程获取了锁,而公平锁则不同,如代码清单。
static finalclass FairSync extends Sync {
privatestaticfinallong serialVersionUID = -3000897897090466540L;
/**
* 公平策略获取锁
*/
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state状态
int c = getState();
// state==0 代表资源可获取
if (c == 0) {
//1.hasQueuedPredecessors判断当前线程是不是CLH队列被唤醒的线程,如果是执行下一个步骤
//2.cas设置state为acquires,acquires传入的是1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
returntrue;
}
}
//如果state!=0,但是当前线程是持有锁线程,直接重入
elseif (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
thrownew Error("Maximum lock count exceeded");
setState(nextc);
returntrue;
}
returnfalse;
}
}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释 放锁之后才能继续获取锁。
之前提到锁(如 Mutex 和 ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线 程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升。
面向接口编程,声明了ReadWriteLock接口,作为读写锁的基本规范。
public interface ReadWriteLock {
/**
* 获取读锁
*/
Lock readLock();
/**
* 获取写锁
*/
Lock writeLock();
}
Java 并发包提供读写锁的实现是 ReentrantReadWriteLock。
废话少说,先上一个读写锁的使用方式。与 ReentrantLock 一样,ReentrantReadWriteLock 的使用方法也是非常简单的,只不过在使用的过程中需要分别派生出“读锁”和“写锁”,在进行共享资源读取操作时,需要使用读锁进行数据同步,在对共享资源进行写操作时,需要使用写锁进行数据一致性的保护.
public class RWDictionary {
// 共享数据
privatefinal Map<String, Data> m = new TreeMap<>();
// 定义读写锁
privatefinal ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 定义读锁
privatefinal Lock r = rwl.readLock();
// 定义写锁
privatefinal Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public List<String> allKeys() {
r.lock();
try {
returnnew ArrayList<>(m.keySet());
} finally {
r.unlock();
}
}
public Data put(String key, Data value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
}
ReentrantReadWriteLock 类有两个内部嵌套类ReadLock和WriteLock,这两个内部类的实例会在 ReentrantReadWriteLock 类的构造器中创建,并通过 ReentrantReadWriteLock 类的readLock()和writeLock()方法访问。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
privatestaticfinallong serialVersionUID = -6992448646407690164L;
/** 内部类,读锁 */
privatefinal ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类,写锁 */
privatefinal ReentrantReadWriteLock.WriteLock writerLock;
/** 内部类,继承 AQS */
final Sync sync;
/**
* 默认非公平策略获取读写锁
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* 公平策略获取读写锁
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
...省略部分代码
}
内部类 Sync 继承 AQS 实现了如下的核心抽象函数。

其中 tryAcquire、release 是为WriteLock写锁准备的。
tryAcquireShared、tryReleaseShared 是为ReadLock读锁准备
我们都知道AQS中维护了一个state状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。
为了支持公平与非公平策略,Sync 扩展了FairSync、NonfairSync子类,两个子类实现了 readerShouldBlock、writerShouldBlock 函数,即读锁与写锁是否阻塞。

ReentrantReadWriteLock 全局图如下,后面只分析非公平锁的加锁和解锁。

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。
如果当 前线程在获取写锁时,读锁已经被获取(读状态不为 0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquire 获取写锁。
protected final boolean tryAcquire(int acquires) {
// 当前线程
Thread current = Thread.currentThread();
int c = getState();
// 计算写锁数量
int w = exclusiveCount(c);
if (c != 0) {
/// 存在读锁或者当前获取线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
returnfalse;
// 超过最大范围
if (w + exclusiveCount(acquires) > MAX_COUNT)
thrownew Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
returntrue;
}
// 写锁是否阻塞或者状态设置失败,返回 false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
returnfalse;
setExclusiveOwnerThread(current);
returntrue;
}

为了易于理解,把它转成流程图,通过流程图,我们发现了一些要点。
获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用unlock函数释放写锁(Lock 接口规范)。
java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryRelease 释放写锁。
写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为 0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对 后续读写线程可见。
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
为了易于理解,把它转成流程图。

任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、 wait(long timeout)、notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以 实现等待/通知模式。
Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等 待/通知模式。
Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。
Condition 对象是由 Lock 对象(调用 Lock 对象的 newCondition()方法)创 建出来的,换句话说,Condition 是依赖 Lock 对象的。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 实例依赖于 lock 实例
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putPtr, takePtr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
// put 时判断是否已经满了
// 则线程在 notFull 条件上排队阻塞
while (count == items.length) {
notFull.await();
}
items[putPtr] = x;
if (++putPtr == items.length) {
putPtr = 0;
}
++count;
// put 成功之后,队列中有元素
// 唤醒在 notEmpty 条件上排队阻塞的线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// take 时,发现为空
// 则线程在 notEmpty 的条件上排队阻塞
while (count == 0) {
notEmpty.await();
}
Object x = items[takePtr];
if (++takePtr == items.length) {
takePtr = 0;
}
--count;
// take 成功,队列不可能是满的
// 唤醒在 notFull 条件上排队阻塞的线程
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
上面是官方文档的一个例子,实现了一个简单的 BlockingQueue ,看懂这里,会发现在同步队列中很多地方都是用的这个逻辑。
在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个 Condition 等待队列。
Condition 条件队列是单链,它没有空的头节点,每个节点都有对应的线程。条件队列头节点和尾节点的指针分别是 firstWaiter 和 lastWaiter ,如下图所示:

当某个线程执行了ConditionObject的await函数,阻塞当前线程,线程会被封装成Node节点添加到条件队列的末端,其他线程执行ConditionObject的signal函数,会将条件队列头部线程节点转移到C H L队列参与竞争资源,具体流程如下图。

调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入条件队列并释 放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相 关联的锁。
调用 Condition 的 signal()方法,将会唤醒在条件队列中等待时间最长的节点(首节点),在 唤醒节点之前,会将节点移到 CLH 同步队列中。
Condition 的 signalAll()方法,相当于对条件队列中的每个节点均执行一次 signal()方法,效 果就是将条件队列中所有节点全部移动到 CLH 同步队列中,并唤醒每个节点的线程。