Skip to content

Java | JUC

更新: 12/22/2025 字数: 0 字 时长: 0 分钟

JUC 是 java.util.concurrent 包的缩写,是 Java 并发编程的核心工具包。它提供了一系列用于处理多线程编程的类和接口,旨在简化并发编程的复杂性,提高程序性能和可靠性。

https://www.bilibili.com/video/BV16J411h7Rd

基本概念

进程与线程

  • 进程
  • 线程
  • 两者对比

并发与并行

  • 并发
  • 并行
  • 串行,单核 CPU 并行在微观上任然是串行

并发(concurrent)是同一时间应对(dealing with)多件事情的能力

并行(parallel)是同一时间动手做(doing)多件事情的能力

同步与异步

需要等待结果返回,才能继续运行就是同步

不需要等待结果返回,就能继续运行就是异步

阻塞与非阻塞

  • IO 操作不占用 CPU,只是我们一般拷贝文件使用的是阻塞 IO,这时相当于线程虽然不用 CPU,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的非阻塞 IO异步 IO优化

创建和运行线程

方法一:直接使用 Thread

java
//创建线程对象
Thread t = new Thread() {
    public void run() {
        //要执行的任务
    }
};

//启动线程
t.start();

方法二:使用 Runnable

  • 把【线程】和【任务】(要执行的代码)分开
  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
java
Runnable runnable = new Runnable() {
    public void run() {
        //要执行的任务
    }
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();

Java8 以后可以使用 lambda 精简代码

java
//创建任务对象
Runnable task2 = () -> log.debug("hello");
//参数 1 是任务对象;参数 2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

原理

  • 方法一,是把线程和任务合并在了一起
  • 方法二,是把线程和任务分开了
  • Runnable 更容易与线程池等高级 API 配合
  • Runnable 让任务类脱离了 Thread 继承体系,更灵活

方法三:FutureTask

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

java
//创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
    log.debug("hello");
    return 100;
});

//参数 1 是任务对象;参数 2 是线程名字,推荐
new Thread(task3, "t3").start();

//主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

查看进程线程的方法

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

Linux

  • ps -fe 查看所有进程
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程

Java

  • ps 命令查看所有 Java 进程
  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类

    shell
    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.passwordjmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole (用户名) , R&D (密码)

线程运行原理

栈与栈帧

JVM 中的栈(Java Virtual Machine Stacks)内存是为线程服务的。每个线程启动后,虚拟机会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

线程上下文切换 (Thread Context Switch)

  • 原因:由于以下一些原因导致 CPU 不再执行当前的线程,转而执行另一个线程的代码

    • 线程的 CPU 时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法
  • 过程:当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

  • 状态:状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。

  • 影响:上下文切换频繁发生会影响性能。

常见方法

方法名功能说明注意
start()启动一个新线程,在新线程中运行 run() 方法中的代码start() 方法只是让线程进入就绪状态,里面的代码不一定立刻运行。每个线程对象的 start() 方法只能调用一次,否则会出现 IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run() 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的几率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep, wait, join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记
interrupted() **(static)*-判断当前线程是否被打断会清除打断标记
currentThread() **(static)*-获取当前正在执行的线程
sleep(long n) **(static)*-让当前执行的线程休眠 n 毫秒,休眠时会让出 cpu 的时间片给其它线程
yield() **(static)*-提示线程调度器让出当前线程对 CPU 的使用主要是为了测试和调试

不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。

方法名功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

主线程与守护线程

默认情况下,Java 进程需要等待所有非守护线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

例:

java
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
    log.debug("开始运行...");
    sleep(2);
    log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();

sleep(1);
log.debug("运行结束...");

注意

  • 垃圾回收器线程就是一种守护线程。
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。

线程状态

五种状态

这是从操作系统层面来描述的。

  • 【初始状态】:仅是在语言层面创建了线程对象,还未与操作系统线程关联。
  • 【可运行状态】(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行。
  • 【运行状态】:指获取了 CPU 时间片运行中的状态。
    • 当 CPU 时间片用完,会从【运行状态】转换为【可运行状态】,会导致线程的上下文切换。
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】。
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换为【可运行状态】。
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。
  • 【终止状态】:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。

六种状态

这是从 *Java 语言- 层面来描述的。

  • 【NEW】:线程刚被创建,但是还没有调用 start() 方法。
  • 【RUNNABLE】:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)。
  • 【BLOCKED】:线程正在等待锁的释放。
  • 【WAITING】:线程进入等待状态,例如调用了 wait()join() 方法。
  • 【TIMED_WAITING】:线程进入限时等待状态,例如调用了 sleep(long) 方法。
  • 【TERMINATED】:线程执行完毕,生命周期结束。

状态转换

情况 1 NEW -> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW -> RUNNABLE

情况 2 RUNNABLE <-> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE -> WAITING
  • 调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程从 WAITING -> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING -> BLOCKED
java
public class TestWaitNotify {
    final static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

情况 3 RUNNABLE <-> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE -> WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING -> RUNNABLE

情况 4 RUNNABLE <-> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE -> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),会让目标线程从 WAITING -> RUNNABLE

情况 5 RUNNABLE <-> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE -> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程从 TIMED_WAITING -> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING -> BLOCKED

情况 6 RUNNABLE <-> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE -> TIMED_WAITING
  • 注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING -> RUNNABLE

情况 7 RUNNABLE <-> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE -> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING -> RUNNABLE

情况 8 RUNNABLE <-> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE -> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING -> RUNNABLE

情况 9 RUNNABLE <-> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE -> BLOCKED
  • obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED -> RUNNABLE,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <-> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

共享模型

  • 上下文切换
  • 临界区
  • 竞态条件

临界区与竞态条件

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案synchronizedLock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意

虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

synchronized

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

synchronized 关键字用在方法上,锁住的是 this 对象。

java
class Test {
    public synchronized void test() {
        // ...
    }
}

// 等价于
class Test {
    public void test() {
        synchronized(this) {
            // ...
        }
    }
}

synchronized 关键字用在静态方法上,锁住的是类对象。

java
class Test {
    public synchronized static void test() {
        // ...
    }
}

// 等价于
class Test {
    public static void test() {
        synchronized(Test.class) {
            // ...
        }
    }
}

线程安全分析

变量与线程安全

成员变量和静态变量

  • 如果它们没有共享,则线程安全。
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:
    • 如果只有读操作,则线程安全。
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全。

局部变量

  • 局部变量是线程安全的。
  • 但局部变量引用的对象则未必:
    • 如果该对象没有逃离方法的作用访问,它是线程安全的。
    • 如果该对象逃离方法的作用范围,需要考虑线程安全。

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的,是指多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:

  • 它们的每个方法是原子的。
  • 注意它们多个方法的组合不是原子的。

Monitor 概念

Java 对象头

以 32 位虚拟机为例

普通对象

txt
|----------------------------------------------------------------|
| Object Header (64 bits)                  |
| ---------------------------------------- | ----------------------- |
| Mark Word (32 bits)                      | Klass Word (32 bits)    |
| ---------------------------------------- | ----------------------- |

数组对象

txt
|----------------------------------------------------------------------------------|
| Object Header (96 bits)          |
| -------------------------------- | ----------------------- | ------------------------- |
| Mark Word (32bits)               | Klass Word (32bits)     | array length (32bits)     |
| -------------------------------- | ----------------------- | ------------------------- |

Mark Word 结构

txt
|-------------------------------------------------------|---------------------|
| Mark Word (32 bits)                                     | State                 |
| ------------------------------------------------------- | --------------------- |
| hashcode:25                                             | age:4                 | biased_lock:0      | 01            | Normal |
| ------------------------------------------------------- | --------------------- |
| thread:23                                               | epoch:2               | age:4              | biased_lock:1 | 01     | Biased |
| ------------------------------------------------------- | --------------------- |
| ptr_to_lock_record:30                                   | 00                    | Lightweight Locked |
| ------------------------------------------------------- | --------------------- |
| ptr_to_heavyweight_monitor:30                           | 10                    | Heavyweight Locked |
| ------------------------------------------------------- | --------------------- |
|                                                         | 11                    | Marked for GC      |
| ------------------------------------------------------- | --------------------- |
txt
|----------------------------------------------------------------------|--------------------|
| Mark Word (64 bits)                                                    | State                |
| ---------------------------------------------------------------------- | -------------------- |
| unused:25                                                              | hashcode:31          | unused:1           | age:4 | biased_lock:0 | 01 | Normal |
| ---------------------------------------------------------------------- | -------------------- |
| thread:54                                                              | epoch:2              | unused:1           | age:4 | biased_lock:1 | 01 | Biased |
| ---------------------------------------------------------------------- | -------------------- |
| ptr_to_lock_record:62                                                  | 00                   | Lightweight Locked |
| ---------------------------------------------------------------------- | -------------------- |
| ptr_to_heavyweight_monitor:62                                          | 10                   | Heavyweight Locked |
| ---------------------------------------------------------------------- | -------------------- |
|                                                                        | 11                   | Marked for GC      |
| ---------------------------------------------------------------------- | -------------------- |

Monitor (重量级锁)

Monitor 被翻译为监视器或管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

Monitor 结构如下:

  • WaitSet:关联了所有处于 WAITING 状态的线程。
  • EntryList:关联了所有处于 BLOCKED 状态的线程,它们都会竞争 Owner 的所有权。
  • Owner:初始时为 null,当有线程占有该 Monitor 时,Owner 指向这个线程。

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

轻量级锁

  • 使用场景:如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁:

java
static final Object obj = new Object();

public static void method1() {
    synchronized(obj) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized(obj) {
        // 同步块 B
    }
}

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

java
static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
  • Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候待锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况

线程 1 (cpu 1 上)对象 Mark线程 2 (cpu 2 上)
-10 (重量锁)-
访问同步块,获取 monitor10 (重量锁) 重量锁指针-
成功 (加锁)10 (重量锁) 重量锁指针-
执行同步块10 (重量锁) 重量锁指针-
执行同步块10 (重量锁) 重量锁指针访问同步块,获取 monitor
执行同步块10 (重量锁) 重量锁指针自旋重试
执行完毕10 (重量锁) 重量锁指针自旋重试
成功 (解锁)01 (无锁)自旋重试
-10 (重量锁) 重量锁指针成功 (加锁)
-10 (重量锁) 重量锁指针执行同步块

自旋重试失败的情况

线程 1 (cpu 1 上)对象 Mark线程 2 (cpu 2 上)
-10 (重量锁)-
访问同步块,获取 monitor10 (重量锁) 重量锁指针-
成功 (加锁)10 (重量锁) 重量锁指针-
执行同步块10 (重量锁) 重量锁指针-
执行同步块10 (重量锁) 重量锁指针访问同步块,获取 monitor
执行同步块10 (重量锁) 重量锁指针自旋重试
执行同步块10 (重量锁) 重量锁指针自旋重试
执行同步块10 (重量锁) 重量锁指针自旋重试
执行同步块10 (重量锁) 重量锁指针阻塞

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

例如:

java
static final Object obj = new Object();
public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}
public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}
public static void m3() {
    synchronized( obj ) {
        // 同步块 C
    }
}

偏向锁状态

一个对象创建时:

  • 如果开启了偏向锁 (默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
  • 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
  • 添加 VM 参数 -XX:-UseBiasedLocking 可以禁用偏向锁,-XX:+UseBiasedLocking 启用偏向锁,默认是启用
  • 升级过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

撤销偏向锁的状态:

  • 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashcode 导致偏向锁被撤销,原因是只有 MarkWord 在 Normal 模式下才能存放下 hashcode
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify,需要申请 Monitor,进入 WaitSet

批量重偏向:

  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 当撤销偏向锁阀值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

批量撤销:

  • 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)

锁粗化

对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化

如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部

锁粗化(Lock Coarsening)是一种 JVM(Java 虚拟机)的优化技术,旨在减少不必要的锁获取和释放操作,从而提高程序的性能。

当 JVM 检测到一系列连续的操作都对同一个锁对象进行加锁和解锁时,它可能会将这些分散的加锁/解锁操作合并(粗化)为一个更大的锁区域,只在整个操作序列的开始处加锁,在结束处解锁。

  • 一些看起来没有加锁的代码,其实隐式的加了很多锁:

    java
    public static String concatString(String s1, String s2, String s3) {
        return s1 + s2 + s3;
    }
  • String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块

    java
    public static String concatString(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以

wait/notify

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

API:

  • obj·wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
  • wait(long n) 有时限的等待,到 n 毫秒后结束等待,或是被 notify

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

sleep(long n)wait(long n)

区别:

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁。

共同点:

  • 它们线程状态都是 TIMED_WAITING(注意:不带参的 wait() 是 WAITING)

正确使用姿势

虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程

解决方法:采用 notifyAll

notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断

解决方法:用 while + wait,当条件不成立,再次 wait

java
synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}

// 另一个线程
synchronized(lock) {
    lock.notifyAll();
}

异步模式

保护性暂停

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

异步模式

生产者/消费者

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

Park/Unpark

基本使用

它们是 LockSupport 类中的方法

java
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);

特点

Objectwait/notify 相比

  • wait, notifynotifyAll 必须配合 Object Monitor 一起使用,而 parkunpark 不必
  • park/unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park/unpark 可以先 unpark,而 wait/notify 不能先 notify(先 notify 是无效的)

多把锁

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

  • t2 线程获得 B 对象锁,接下来想获取 A 对象的锁
  • t1 线程获得 A 对象锁,接下来想获取 B 对象的锁

定位死锁:

  • 检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

活锁

活锁出现在两个线程互相改变对方的结束条件,最后准也无法结束

饥饿锁

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

ReentrantLock

相对于 synchronized 它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

synchronized 一样,都支持可重入。

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它就是这把锁的拥有者,因此有权利再次获取这把锁。
  • 如果不是可重入锁,那么第二次获得锁时,自己也会被锁挡住。

可打断

public void lockInterruptibly():获得可打断的锁

  • 如果没有竞争此方法就会获取 lock 对象锁
  • 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断

锁超时

public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列

public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出

注意:tryLock 期间也可以被打断

公平锁

构造方法:ReentrantLock lock = new ReentrantLock(true)

java
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 默认是不公平的:

java
public ReentrantLock() {
    sync = new NonfairSync();
}

说明:公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用流程:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock
  • 竞争 lock 锁成功后,从 await 后继续执行

Java 内容模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性-保证指令不会受到线程上下文切换的影响
  • 可见性-保证指令不会受 cpu 缓存的影响
  • 有序性-保证指令不会受 cpu 指令并行优化的影响

可见性

是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题

main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

java
static boolean run = true;      // 添加 volatile
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
        // ....
        }
    });
    t.start();
    sleep(1);
    run = false;    // 线程 t 不会如预想的停下来
}

原因:

  • 初始状态,t 线程刚开始从主内存读取了 run 的值到工作内存
  • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

解决方法

volatile(易变关键字):它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

原子性

原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响

定义原子操作的使用规则:

  1. 不允许 read 和 load、store 和 write 操作之一单独出现,必须顺序执行,但是不要求连续
  2. 不允许一个线程丢弃 assign 操作,必须同步回主存
  3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中
  4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作
  5. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有**执行相同次数的 unlock*- 操作,变量才会被解锁,lock 和 unlock 必须成对出现
  6. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新从主存加载
  7. 如果一个变量事先没有被 lock 操作锁定,则不允许执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量
  8. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

有序性

有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序

CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种:

java
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为五级指令流水线。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率

处理器在进行重排序时,必须要考虑指令之间的数据依赖性

  • 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致
  • 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行

补充知识:

  • 指令周期是取出一条指令并执行这条指令的时间,一般由若干个机器周期组成
  • 机器周期也称为 CPU 周期,一条指令的执行过程划分为若干个阶段(如取指、译码、执行等),每一阶段完成一个基本操作,完成一个基本操作所需要的时间称为机器周期
  • 振荡周期指周期性信号作周期性重复变化的时间间隔

Balking

Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

java
public class MonitorService {
    // 用来表示是否已经有线程已经在执行启动了
    private volatile boolean starting = false;
    public void start() {
        System.out.println("尝试启动监控线程...");
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }
        // 真正启动监控线程...
    }
}

对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待

例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题:

  • 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为 false,则 t2 就又初始化一次
  • volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁
java
public class TestVolatile {
    volatile boolean initialized = false;
    
    void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }
    private void doInit() {
    }
}

volatile

同步机制

volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性
  • 保证有序性(禁止指令重排)

性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小

synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性

  • 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
  • 线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)

指令重排

volatile 修饰的变量,可以禁用指令重排

指令重排实例:

  • example 1:

    java
    public void mySort() {
      int x = 11; //语句 1
      int y = 12; //语句 2  谁先执行效果一样
      x = x + 5;  //语句 3
      y = x - x;  //语句 4
    }

    执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4

    指令重排也有限制不会出现:4321,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行

  • example 2:

    java
    int num = 0;
    boolean ready = false;
    // 线程 1 执行此方法
    public void actor1(I_Result r) {
        if(ready) {
          r.r1 = num + num;
        } else {
          r.r1 = 1;
        }
    }
    // 线程 2 执行此方法
    public void actor2(I_Result r) {
      num = 2;
      ready = true;
    }

    情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1

    情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1

    情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4

    情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排

底层原理

缓存一致

使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据

lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

内存屏障有三个作用:

  • 确保对内存的读 - 改 - 写操作原子执行
  • 阻止屏障两侧的指令重排序
  • 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效
内存屏障

保证可见性

  • 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    java
    public void actor2(I_Result r) {
        num = 2;
        ready = true; // ready 是 volatile 赋值带写屏障
        // 写屏障
    }
  • 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据

    java
    public void actor1(I_Result r) {
        // 读屏障
        // ready 是 volatile 读取值带读屏障
        if(ready) {
          r.r1 = num + num;
        } else {
          r.r1 = 1;
        }
    }

  • 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能

保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前

  • 有序性的保证也只是保证了本线程内相关代码不被重排序

    java
    volatile i = 0;
    new Thread(() -> {i++});
    new Thread(() -> {i--});

    i++ 反编译后的指令:

    java
    0: iconst_1           // 当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中
    1: istore_1           // 将操作数栈顶数据弹出,存入局部变量表的 slot 1
    2: iinc       1, 1

happens-before

happens-before 是 Java 内存模型(JMM)中定义的一系列规则,用于阐述操作之间的内存可见性。它解决了在多线程环境下,一个线程对共享变量的修改何时对另一个线程可见的问题。

简单来说,如果两个操作存在 happens-before 关系,那么前一个操作的执行结果对后一个操作是可见的。这是一种偏序关系。

把这个概念想象成一个“时间胶囊”。线程 A 把变量 x 的值从 0 改为 10,然后把这个结果(连同其他所有修改)一起打包放进一个“时间胶囊”里,并盖上了时间戳。如果线程 B 的某个操作被规定在线程 A 的这个操作“之后”发生(即满足 happens-before 规则),那么线程 B 就有权打开这个“时间胶囊”,并且能看到 x 的值已经是 10 了。

JMM 提供了以下几种天然的 happens-before 规则,你不需要 synchronized 也能保证可见性:

  1. 程序次序规则:在一个线程内,书写在前面的操作 happens-before 于书写在后面的操作。这是最符合我们单线程编程思维的一条。
  2. 管程锁定规则:一个 unlock(解锁)操作 happens-before 于后面对同一个锁的 lock(加锁)操作。也就是说,如果一个线程释放了锁,那么它在临界区内做的所有修改,对下一个获得该锁的线程来说都是可见的。
  3. volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个变量的读操作。这意味着一旦一个线程修改了 volatile 变量,新值对其他线程是立即可见的。
  4. 线程启动规则:线程对象的 start() 方法 happens-before 于此线程的每一个动作。
  5. 线程终止规则:线程中的所有操作都 happens-before 于对此线程的终止检测。我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程 interrupt() 方法的调用 happens-before 于被中断线程的代码检测到中断事件的发生。
  7. 对象终结规则:一个对象的初始化完成 happens-before 于它的 finalize() 方法的开始。
  8. 传递性:如果操作 A happens-before 操作 B,操作 B happens-before 操作 C,那么可以得出操作 A happens-before 操作 C。

无锁

CAS

无锁编程:Lock Free

CAS 的全称是 Compare-And-Swap(比较并替换值)或者 Compare-And-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 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
  • CAS 体现的是无锁并发、无阻塞并发,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
  • CAS 是基于乐观锁的思想

CAS 缺点:

  • 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,使用 CAS 线程数不要超过 CPU 的核心数,采用分段 CAS 和自动迁移机制
  • 只能保证一个共享变量的原子操作
    • 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
    • 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来 ABA 问题

注意:

  • CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

CAS 与 synchronized 总结:

  • synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差
  • CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值,CAS 这种机制也称之为乐观锁,综合性能较好

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

原理分析

AtomicInteger 原理:自旋锁 + CAS 算法

CAS 算法:有 3 个操作数(内存值 V,旧的预期值 A,要修改的值 B)

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

分析 getAndSet 方法:

  • AtomicInteger:

    java
    public final int getAndSet(int newValue) {
        /**
        * this:       当前对象
        * valueOffset:    内存偏移量,内存地址
        */
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

    java
    valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
    //调用本地方法   -->
    public native long objectFieldOffset(Field var1);
  • unsafe 类:

    java
    // val1: AtomicInteger 对象本身,var2: 该对象值得引用地址,var4: 需要变动的数
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // var5: 用 var1 和 var2 找到的内存中的真实值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }

    var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据

  • 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量

    java
    private volatile int value

    CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果

分析 getAndUpdate 方法:

  • getAndUpdate:

    java
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get(); //当前值,cas 的期望值
            next = updateFunction.applyAsInt(prev);//期望值更新到该值
        } while (!compareAndSet(prev, next));//自旋
        return prev;
    }

    函数式接口:可以自定义操作逻辑

    java
    AtomicInteger a = new AtomicInteger();
    a.getAndUpdate(i -> i + 10);
  • compareAndSet:

    java
    public final boolean compareAndSet(int expect, int update) {
        /**
        * this:           当前对象
        * valueOffset:    内存偏移量,内存地址
        * expect:         期望的值
        * update:         更新的值
        */
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

原子引用

原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量

原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference

AtomicReference 类:

  • 构造方法:AtomicReference<T> atomicReference = new AtomicReference<T>()

  • 常用 API:

    • public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作
    • public final void set(V newValue):将值设置为 newValue
    • public final V get():返回当前值
java
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Student s1 = new Student(33, "z3");
        
        // 创建原子引用包装类
        AtomicReference<Student> atomicReference = new AtomicReference<>();
        // 设置主内存共享变量为 s1
        atomicReference.set(s1);

        // 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4
        while (true) {
            Student s2 = new Student(44, "l4");
            if (atomicReference.compareAndSet(s1, s2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

class Student {
    private int id;
    private String name;
    // ...
}

原子数组

原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

AtomicIntegerArray 类方法:

java
/**
*   i       the index
* expect    the expected value
* update    the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

原子更新器

原子更新器类: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
java
public class UpdateDemo {
    private volatile int field;
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
                    .newUpdater(UpdateDemo.class, "field");
        UpdateDemo updateDemo = new UpdateDemo();
        fieldUpdater.compareAndSet(updateDemo, 0, 10);
        System.out.println(updateDemo.field);//10
    }
}

原子累加器

原子累加器类: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

final

原理

java
public class TestFinal {
    final int a = 20;
}

字节码:

java
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20        // 将值直接放入栈中
7: putfield #2      // Field a:I
<-- 写屏障
10: return

final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况

其他线程访问 final 修饰的变量

  • 复制一份放入栈中直接访问,效率高
  • 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取

不可变

不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象

不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式

String 类也是不可变的,该类和类中所有属性都是 final 的

  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

  • 无写入方法(set)确保外部不能对内部属性进行修改

  • 属性用 final 修饰保证了该属性是只读的,不能修改

    java
    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        //....
    }
  • 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过创建副本对象来避免共享的方式称之为保护性拷贝

State

无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量

Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的

ABA

ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值

其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号,使用新的原子工具类 AtomicStampedReference

  • 构造方法:

    • public AtomicStampedReference(V initialRef, int initialStamp):初始值和初始版本号
  • 常用 API:

    • public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)期望引用和期望版本号都一致才进行 CAS 修改数据
    • public void set(V newReference, int newStamp):设置值和版本号
    • public V getReference():返回引用的值
    • public int getStamp():返回当前版本号
java
public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
    int startStamp = atomicReference.getStamp();
    new Thread(() ->{
        int stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(100, 101, stamp, stamp + 1);
        stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(101, 100, stamp, stamp + 1);
    },"t1").start();

    new Thread(() ->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {
            System.out.println(atomicReference.getReference());//100
            System.out.println(Thread.currentThread().getName() + "线程修改失败");
        }
    },"t2").start();
}

Unsafe

Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问

Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针

模拟实现原子整数:

java
public static void main(String[] args) {
    MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
    if (atomicInteger.compareAndSwap(20)) {
        System.out.println(atomicInteger.getValue());
    }
}

class MyAtomicInteger {
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
    private volatile int value;

    static {
        try {
            //Unsafe unsafe = Unsafe.getUnsafe() 这样会报错,需要反射获取
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
            // 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值
            VALUE_OFFSET = UNSAFE.objectFieldOffset(
                           MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }

    public boolean compareAndSwap(int update) {
        while (true) {
            int prev = this.value;
            int next = update;
            //                          当前对象  内存偏移量    期望值 更新值
            if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
                System.out.println("CAS 成功");
                return true;
            }
        }
    }
}

线程池

享元模式

享元模式(Flyweight pattern):用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式

异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池

工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象

自定义连接池:

java
public static void main(String[] args) {
    Pool pool = new Pool(2);
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            Connection con = pool.borrow();
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pool.free(con);
        }).start();
    }
}
class Pool {
    //连接池的大小
    private final int poolSize;
    //连接对象的数组
    private Connection[] connections;
    //连接状态数组 0 表示空闲  1 表示繁忙
    private AtomicIntegerArray states;  //int[] -> AtomicIntegerArray

    //构造方法
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("连接" + (i + 1));
        }
    }

    //使用连接
    public Connection borrow() {
        while (true) {
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        System.out.println(Thread.currentThread().getName() + " borrow " +  connections[i]);
                        return connections[i];
                    }
                }
            }
            //如果没有空闲连接,当前线程等待
            synchronized (this) {
                try {
                    System.out.println(Thread.currentThread().getName() + " wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //归还连接
    public void free(Connection con) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == con) {//判断是否是同一个对象
                states.set(i, 0);//不用 cas 的原因是只会有一个线程使用该连接
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName() + " free " + con);
                    this.notifyAll();
                }
                break;
            }
        }
    }

}

class MockConnection implements Connection {
    private String name;
    //.....
}

线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

状态名高 3 位接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011--终结状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

java
// c 为旧值,ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)));

// rs 为高 3 位代表线程池状态,wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }
  • rs: running status
  • wc: working count

构造方法

java
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

线程池工作流程及拒绝策略

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
  • 如果线程到达 maximumPoolSize 仍然有新任务,这时会执行拒绝策略。JDK 提供了 4 种实现,其它著名框架也提供了实现:
    • AbortPolicy: 让调用者抛出 RejectedExecutionException 异常,这是默认策略。
    • CallerRunsPolicy: 让调用者运行任务。
    • DiscardPolicy: 放弃本次任务。
    • DiscardOldestPolicy: 放弃队列中最早的任务,本任务取而代之。
    • Dubbo 的实现: 在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题。
    • Netty 的实现: 是创建一个新线程来执行任务。
    • ActiveMQ 的实现: 带超时等待 (60s) 尝试放入队列,类似我们之前自定义的拒绝策略。
    • PinPoint 的实现: 它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略。
  • 当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTimeunit 来控制。

newFixedThreadPool

代码示例:

java
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

特点:

  • 核心线程数 == 最大线程数 (没有救急线程创建),因此也无需超时时间。
  • 阻塞队列是无界的,可以放任意数量的任务。

评价:

  • 适用于任务量已知,相对耗时的任务。

newCachedThreadPool

代码示例:

java
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

特点:

  • 核心线程数是 0,最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,这意味着:
    • 全部都是救急线程 (60s 后可以回收)。
    • 救急线程可以无限创建。
  • 队列采用了 SynchronousQueue 实现,特点是它没有容量,没有线程来取是放不进去的 (一手交钱、一手交货)。

评价:

  • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1 分钟后释放线程。
  • 适合任务数比较密集,但每个任务执行时间较短的情况。

newSingleThreadExecutor

代码示例:

java
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

使用场景:

  • 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务:如果任务执行失败而终止,那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
  • Executors.newSingleThreadExecutor() 线程个数始终为 1,不能修改。
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。
  • Executors.newFixedThreadPool(1) 初始时为 1,以后还可以修改。
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

提交任务

  • 执行任务

    java
    void execute(Runnable command);
  • 提交任务 task,用返回值 Future 获得任务执行结果

    java
    <T> Future<T> submit(Callable<T> task);
  • 提交 tasks 中所有任务

    java
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
  • 提交 tasks 中所有任务,带超时时间

    java
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
  • 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消

    java
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
  • 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间

    java
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

停止方法

  • shutdown():停止线程池

    java
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        // 获取线程池全局锁
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 设置线程池状态为 SHUTDOWN,如果线程池状态大于 SHUTDOWN,就不会设置直接返回
            advanceRunState(SHUTDOWN);
            // 中断空闲线程
            interruptIdleWorkers();
            // 空方法,子类可以扩展
            onShutdown(); 
        } finally {
            // 释放线程池全局锁
            mainLock.unlock();
        }
        tryTerminate();
    }
  • interruptIdleWorkers():shutdown 方法会中断所有空闲线程,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕

    java
    // onlyOne == true 说明只中断一个线程,false 则中断所有线程
    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        / /持有全局锁
        mainLock.lock();
        try {
            // 遍历所有 worker
            for (Worker w : workers) {
                // 获取当前 worker 的线程
                Thread t = w.thread;
                // 条件一成立:说明当前迭代的这个线程尚未中断
                // 条件二成立:说明【当前 worker 处于空闲状态】,阻塞在 poll 或者 take,因为 worker 执行 task 时是要加锁的
                //           每个 worker 有一个独占锁,w.tryLock() 尝试加锁,加锁成功返回 true
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        // 释放 worker 的独占锁
                        w.unlock();
                    }
                }
                // false,代表中断所有的线程
                if (onlyOne)
                    break;
            }
    
        } finally {
            // 释放全局锁
            mainLock.unlock();
        }
    }
  • shutdownNow():直接关闭线程池,不会等待任务执行完成

    java
    public List<Runnable> shutdownNow() {
        // 返回值引用
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        // 获取线程池全局锁
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 设置线程池状态为 STOP
            advanceRunState(STOP);
            // 中断线程池中【所有线程】
            interruptWorkers();
            // 从阻塞队列中导出未处理的 task
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
    
        tryTerminate();
        // 返回当前任务队列中 未处理的任务。
        return tasks;
    }
  • tryTerminate():设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)

    java
    final void tryTerminate() {
        for (;;) {
            // 获取 ctl 的值
            int c = ctl.get();
            // 线程池正常,或者有其他线程执行了状态转换的方法,当前线程直接返回
            if (isRunning(c) || runStateAtLeast(c, TIDYING) ||
                // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            
            // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空
            // 判断线程池中线程的数量
            if (workerCountOf(c) != 0) {
                // 【中断一个空闲线程】,在 queue.take() | queue.poll() 阻塞空闲
                // 唤醒后的线程会在 getTask() 方法返回 null,
                // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
            // 池中的线程数量为 0 来到这里
            final ReentrantLock mainLock = this.mainLock;
            // 加全局锁
            mainLock.lock();
            try {
                // 设置线程池状态为 TIDYING 状态,线程数量为 0
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        // 结束线程池
                        terminated();
                    } finally {
                        // 设置线程池状态为 TERMINATED 状态。
                        ctl.set(ctlOf(TERMINATED, 0));
                        // 【唤醒所有调用 awaitTermination() 方法的线程】
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                // 释放线程池全局锁
                mainLock.unlock();
            }
        }
    }

任务调度

Timer

Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务

java
private static void method1() {
    Timer timer = new Timer();
    TimerTask task1 = new TimerTask() {
        @Override
        public void run() {
            System.out.println("task 1");
            //int i = 1 / 0;//任务一的出错会导致任务二无法执行
            Thread.sleep(2000);
        }
    };
    TimerTask task2 = new TimerTask() {
        @Override
        public void run() {
            System.out.println("task 2");
        }
    };
    // 使用 timer 添加两个任务,希望它们都在 1s 后执行
    // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务 1 的延时,影响了任务 2 的执行
    timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1
    timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2
}

Scheduled

任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor:

  • 使用内部类 ScheduledFutureTask 封装任务
  • 使用内部类 DelayedWorkQueue 作为线程池队列
  • 重写 onShutdown 方法去处理 shutdown 后的任务
  • 提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法,以便开发者进行扩展

构造方法:Executors.newScheduledThreadPool(int corePoolSize)

java
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 最大线程数固定为 Integer.MAX_VALUE,保活时间 keepAliveTime 固定为 0
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          // 阻塞队列是 DelayedWorkQueue
          new DelayedWorkQueue());
}

常用 API:

  • ScheduledFuture<?> schedule(Runnable/Callable<V>, long delay, TimeUnit u):延迟执行任务
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable/Callable<V>, long initialDelay, long period, TimeUnit unit):定时执行周期任务,不考虑执行的耗时,参数为初始延迟时间、间隔时间、单位
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable/Callable<V>, long initialDelay, long delay, TimeUnit unit):定时执行周期任务,考虑执行的耗时,参数为初始延迟时间、间隔时间、单位

基本使用:

  • 延迟任务,但是出现异常并不会在控制台打印,也不会影响其他线程的执行

    java
    public static void main(String[] args){
        // 线程池大小为 1 时也是串行执行
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        // 添加两个任务,都在 1s 后同时执行
        executor.schedule(() -> {
            System.out.println("任务 1,执行时间:" + new Date());
            //int i = 1 / 0;
            try { Thread.sleep(2000); } catch (InterruptedException e) { }
        }, 1000, TimeUnit.MILLISECONDS);
        
        executor.schedule(() -> {
            System.out.println("任务 2,执行时间:" + new Date());
        }, 1000, TimeUnit.MILLISECONDS);
    }
  • 定时任务 scheduleAtFixedRate:一次任务的启动到下一次任务的启动之间只要大于等于间隔时间,抢占到 CPU 就会立即执行

    java
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        System.out.println("start..." + new Date());
        
        pool.scheduleAtFixedRate(() -> {
            System.out.println("running..." + new Date());
            Thread.sleep(2000);
        }, 1, 1, TimeUnit.SECONDS);
    }
    
    /*start...Sat Apr 24 18:08:12 CST 2021
    running...Sat Apr 24 18:08:13 CST 2021
    running...Sat Apr 24 18:08:15 CST 2021
    running...Sat Apr 24 18:08:17 CST 2021
  • 定时任务 scheduleWithFixedDelay:一次任务的结束到下一次任务的启动之间等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔

    java
    public static void main(String[] args){
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
        System.out.println("start..." + new Date());
        
        pool.scheduleWithFixedDelay(() -> {
            System.out.println("running..." + new Date());
            Thread.sleep(2000);
        }, 1, 1, TimeUnit.SECONDS);
    }
    /*start...Sat Apr 24 18:11:41 CST 2021
    running...Sat Apr 24 18:11:42 CST 2021
    running...Sat Apr 24 18:11:45 CST 2021
    running...Sat Apr 24 18:11:48 CST 2021

同步器

AQS

核心思想

AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器

AQS 用状态属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

  • 独占模式是只有一个线程能够访问资源,如 ReentrantLock
  • 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式

AQS 核心思想:

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态

  • 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中

    CLH 是一种基于单向链表的高性能、公平的自旋锁,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

设计原理

设计原理:

  • 获取锁:

    java
    while(state 状态不允许获取) { // tryAcquire(arg)
        if(队列中还没有此线程) {
            入队并阻塞 park
        }
    }
    当前线程出队
  • 释放锁:

    java
    if(state 状态允许了) {  // tryRelease(arg)
        恢复阻塞的线程 (s) unpark
    }

AbstractQueuedSynchronizer 中 state 设计:

  • state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态

    java
    private volatile int state;
  • 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 节点的状态,有以下几种状态:

    java
    // 默认为 0
    volatile int waitStatus;
    // 由于超时或中断,此节点被取消,不会再改变状态
    static final int CANCELLED =  1;
    // 此节点后面的节点已(或即将)被阻止(通过 park),【当前节点在释放或取消时必须唤醒后面的节点】
    static final int SIGNAL    = -1;
    // 此节点当前在条件队列中
    static final int CONDITION = -2;
    // 将 releaseShared 传播到其他节点
    static final int PROPAGATE = -3;

阻塞恢复设计:

  • 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果
  • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
  • park 线程可以通过 interrupt 打断

队列设计:

  • 使用了 FIFO 先入先出队列,并不支持优先级队列,同步队列是双向链表,便于出队入队

    java
    // 头结点,指向哑元节点
    private transient volatile Node head;
    // 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列
    private transient volatile Node tail;
    
    static final class Node {
        // 枚举:共享模式
        static final Node SHARED = new Node();
        // 枚举:独占模式
        static final Node EXCLUSIVE = null;
        // node 需要构建成 FIFO 队列,prev 指向前继节点
        volatile Node prev;
        // next 指向后继节点
        volatile Node next;
        // 当前 node 封装的线程
        volatile Thread thread;
        // 条件队列是单向链表,只有后继指针,条件队列使用该属性
        Node nextWaiter;
    }

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,条件队列是单向链表

    java
    public class ConditionObject implements Condition, java.io.Serializable {
        // 指向条件队列的第一个 node 节点
        private transient Node firstWaiter;
        // 指向条件队列的最后一个 node 节点
        private transient Node lastWaiter;
    }

模板对象

同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码

  • 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法
  • 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

java
isHeldExclusively()     //该线程是否正在独占资源。只有用到 condition 才需要去实现它
tryAcquire(int)         //独占方式。尝试获取资源,成功则返回 true,失败则返回 false
tryRelease(int)         //独占方式。尝试释放资源,成功则返回 true,失败则返回 false
tryAcquireShared(int)   //共享方式。尝试获取资源。负数表示失败;0 表示成功但没有剩余可用资源;正数表示成功且有剩余资源
tryReleaseShared(int)   //共享方式。尝试释放资源,成功则返回 true,失败则返回 false
  • 默认情况下,每个方法都抛出 UnsupportedOperationException
  • 这些方法的实现必须是内部线程安全的
  • AQS 类中的其他方法都是 final,所以无法被其他类使用,只有这几个方法可以被其他类使用

自定义

自定义一个不可重入锁:

java
class MyLock implements Lock {
    //独占锁 不可重入
    class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                // 加上锁 设置 owner 为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        @Override   //解锁
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);//volatile 修饰的变量放在后面,防止指令重排
            return true;
        }
        @Override   //是否持有独占锁
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    @Override   //加锁(不成功进入等待队列等待)
    public void lock() {
        sync.acquire(1);
    }

    @Override   //加锁 可打断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override   //尝试加锁,尝试一次
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override   //尝试加锁,带超时
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    
    @Override   //解锁
    public void unlock() {
        sync.release(1);
    }
    
    @Override   //条件变量
    public Condition newCondition() {
        return sync.newCondition();
    }
}

Re-Lock

锁对比

ReentrantLock 相对于 synchronized 具备如下特点:

  1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
  2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同
  3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
  4. 可中断:ReentrantLock 可中断,而 synchronized 不行
  5. 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
    • ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
    • 不公平锁的含义是阻塞队列内公平,队列外非公平
  6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列
    • ReentrantLock 可以设置超时时间,synchronized 会一直等待
  7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程
  8. 两者都是可重入锁

使用锁

构造方法:ReentrantLock lock = new ReentrantLock();

ReentrantLock 类 API:

  • public void lock():获得锁

    • 如果锁没有被另一个线程占用,则将锁定计数设置为 1

    • 如果当前线程已经保持锁定,则保持计数增加 1

    • 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态

  • public void unlock():尝试释放锁

    • 如果当前线程是该锁的持有者,则保持计数递减
    • 如果保持计数现在为零,则锁定被释放
    • 如果当前线程不是该锁的持有者,则抛出异常

基本语法:

java
// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}

公平锁

基本使用

构造方法:ReentrantLock lock = new ReentrantLock(true)

java
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 默认是不公平的:

java
public ReentrantLock() {
    sync = new NonfairSync();
}

说明:公平锁一般没有必要,会降低并发度

非公原理
加锁

NonfairSync 继承自 AQS

java
public void lock() {
    sync.lock();
}
  • 没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1

    java
    // ReentrantLock.NonfairSync#lock
    final void lock() {
        // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】
        if (compareAndSetState(0, 1))
            // 设置当前线程为独占线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);//失败进入
    }
  • 第一个竞争出现:Thread-1 执行,CAS 尝试将 state 由 0 改为 1,结果失败(第一次),进入 acquire 逻辑

    java
    // AbstractQueuedSynchronizer#acquire
    public final void acquire(int arg) {
        // tryAcquire 尝试获取锁失败时,会调用 addWaiter 将当前线程封装成 node 入队,acquireQueued 阻塞当前线程,
        // acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果线程被中断了逻辑来到这,完成一次真正的打断效果
            selfInterrupt();
    }
  • 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是 1,结果仍然失败(第二次),加锁成功有两种情况:

    • 当前 AQS 处于无锁状态

    • 加锁线程就是当前线程,说明发生了锁重入

      java
      // ReentrantLock.NonfairSync#tryAcquire
      protected final boolean tryAcquire(int acquires) {
          return nonfairTryAcquire(acquires);
      }
      // 抢占成功返回 true,抢占失败返回 false
      final boolean nonfairTryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          // state 值
          int c = getState();
          // 条件成立说明当前处于【无锁状态】
          if (c == 0) {
              //如果还没有获得锁,尝试用 cas 获得,这里体现非公平性:不去检查 AQS 队列是否有阻塞线程直接获取锁        
              if (compareAndSetState(0, acquires)) {
                  // 获取锁成功设置当前线程为独占锁线程。
                  setExclusiveOwnerThread(current);
                  return true;
              }    
          }    
              // 如果已经有线程获得了锁,独占锁线程还是当前线程,表示【发生了锁重入】
          else if (current == getExclusiveOwnerThread()) {
              // 更新锁重入的值
              int nextc = c + acquires;
              // 越界判断,当重入的深度很深时,会导致 nextc < 0,int 值达到最大之后再 + 1 变负数
              if (nextc < 0) // overflow
                  throw new Error("Maximum lock count exceeded");
              // 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内
              setState(nextc);
              return true;
          }
          // 获取失败
          return false;
      }
  • 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁

    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态

    • Node 的创建是懒惰的,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

      java
      // AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点
      private Node addWaiter(Node mode) {
          // 将当前线程关联到一个 Node 对象上,模式为独占模式   
          Node node = new Node(Thread.currentThread(), mode);
          Node pred = tail;
          // 快速入队,如果 tail 不为 null,说明存在队列
          if (pred != null) {
              // 将当前节点的前驱节点指向 尾节点
              node.prev = pred;
              // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】
              if (compareAndSetTail(pred, node)) {
                  pred.next = node;// 双向链表
                  return node;
              }
          }
          // 初始时队列为空,或者 CAS 失败进入这里
          enq(node);
          return node;
      }
      java
      // AbstractQueuedSynchronizer#enq
      private Node enq(final Node node) {
          // 自旋入队,必须入队成功才结束循环
          for (;;) {
              Node t = tail;
              // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】
              if (t == null) {
                  // 设置一个【哑元节点】,头尾指针都指向该节点
                  if (compareAndSetHead(new Node()))
                      tail = head;
              } else {
                  // 自旋到这,普通入队方式,首先赋值尾节点的前驱节点【尾插法】
                  node.prev = t;
                  // 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】
                  if (compareAndSetTail(t, node)) {
                      //【此时 t.next  = null,并且这里已经 CAS 结束,线程并不是安全的】
                      t.next = node;
                      return t;   // 返回当前 node 的前驱节点
                  }
              }
          }
      }
  • 线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程

    • acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞

    • 如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次)

      java
      final boolean acquireQueued(final Node node, int arg) {
          // true 表示当前线程抢占锁失败,false 表示成功
          boolean failed = true;
          try {
              // 中断标记,表示当前线程是否被中断
              boolean interrupted = false;
              for (;;) {
                  // 获得当前线程节点的前驱节点
                  final Node p = node.predecessor();
                  // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
                  if (p == head && tryAcquire(arg)) {
                      // 获取成功,设置当前线程自己的 node 为 head
                      setHead(node);
                      p.next = null; // help GC
                      // 表示抢占锁成功
                      failed = false;
                      // 返回当前线程是否被中断
                      return interrupted;
                  }
                  // 判断是否应当 park,返回 false 后需要新一轮的循环,返回 true 进入条件二阻塞线程
                  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                      // 条件二返回结果是当前线程是否被打断,没有被打断返回 false 不进入这里的逻辑
                      // 【就算被打断了,也会继续循环,并不会返回】
                      interrupted = true;
              }
          } finally {
              // 【可打断模式下才会进入该逻辑】
              if (failed)
                  cancelAcquire(node);
          }
      }
    • 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点

      java
      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          int ws = pred.waitStatus;
          // 表示前置节点是个可以唤醒当前节点的节点,返回 true
          if (ws == Node.SIGNAL)
              return true;
          // 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试
          if (ws > 0) {
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              // 获取到非取消的节点,连接上当前节点
              pred.next = node;
          // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑
          } else {
              // 【设置上一个节点状态为 Node.SIGNAL】,返回外层循环重试
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          // 返回不应该 park,再次尝试一次
          return false;
      }
    • shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)

    • 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true

    • 进入 parkAndCheckInterrupt,Thread-1 park(灰色表示)

      java
      private final boolean parkAndCheckInterrupt() {
          // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
          LockSupport.park(this);
          // 判断当前线程是否被打断,清除打断标记
          return Thread.interrupted();
      }
  • 再有多个线程经历竞争失败后:

解锁

ReentrantLock#unlock:释放锁

java
public void unlock() {
    sync.release(1);
}

Thread-0 释放锁,进入 release 流程

  • 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0

  • 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor

    java
    // AbstractQueuedSynchronizer#release
    public final boolean release(int arg) {
        // 尝试释放锁,tryRelease 返回 true 表示当前线程已经【完全释放锁,重入的释放了】
        if (tryRelease(arg)) {
            // 队列头节点
            Node h = head;
            // 头节点什么时候是空?没有发生锁竞争,没有竞争线程创建哑元节点
            // 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }    
        return false;
    }
    java
    // ReentrantLock.Sync#tryRelease
    protected final boolean tryRelease(int releases) {
        // 减去释放的值,可能重入
        int c = getState() - releases;
        // 如果当前线程不是持有锁的线程直接报错
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        // 是否已经完全释放锁
        boolean free = false;
        // 支持锁重入,只有 state 减为 0, 才完全释放锁成功
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        // 当前线程就是持有锁线程,所以可以直接更新锁,不需要使用 CAS
        setState(c);
        return free;
    }
  • 进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点

    • 找到队列中距离 head 最近的一个没取消的 Node,unpark 恢复其运行,本例中即为 Thread-1

    • 回到 Thread-1 的 acquireQueued 流程

      java
      private void unparkSuccessor(Node node) {
          // 当前节点的状态
          int ws = node.waitStatus;    
          if (ws < 0)        
              // 【尝试重置状态为 0】,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了
              compareAndSetWaitStatus(node, ws, 0);    
          // 找到需要 unpark 的节点,当前节点的下一个    
          Node s = node.next;    
          // 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点
          if (s == null || s.waitStatus > 0) {
              s = null;
              // AQS 队列【从后至前】找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了
              for (Node t = tail; t != null && t != node; t = t.prev)
                  // 说明当前线程状态需要被唤醒
                  if (t.waitStatus <= 0)
                      // 置换引用
                      s = t;
          }
          // 【找到合适的可以被唤醒的 node,则唤醒线程】
          if (s != null)
              LockSupport.unpark(s.thread);
      }

    从后向前的唤醒的原因: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 竞争

java
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点,没有 (false) 才去竞争
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 锁重入
        return false;
    }
}
java
public final boolean hasQueuedPredecessors() {    
    Node t = tail;
    Node h = head;
    Node s;    
    // 头尾指向一个节点,链表为空,返回 false
    return h != t &&
        // 头尾之间有节点,判断头节点的下一个是不是空
        // 不是空进入最后的判断,第二个节点的线程是否是本线程,不是返回 true,表示当前节点有前驱节点
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁

源码解析参考:nonfairTryAcquire(int acquires))tryRelease(int releases)

java
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
    method1();
}
public static void method1() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + " execute method1");
        method2();
    } finally {
        lock.unlock();
    }
}
public static void method2() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + " execute method2");
    } finally {
        lock.unlock();
    }
}

在 Lock 方法加两把锁会是什么情况呢?

  • 加锁两次解锁两次:正常执行
  • 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明申请几把锁,最后需要解除几把锁
  • 加锁一次解锁两次:运行程序会直接报错
java
public void getLock() {
    lock.lock();
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "\t get Lock");
    } finally {
        lock.unlock();
        //lock.unlock();
    }
}

可打断

基本使用

public void lockInterruptibly():获得可打断的锁

  • 如果没有竞争此方法就会获取 lock 对象锁
  • 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断

java
public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();    
    Thread t1 = new Thread(() -> {        
        try {            
            System.out.println("尝试获取锁");            
            lock.lockInterruptibly();        
        } catch (InterruptedException e) {            
            System.out.println("没有获取到锁,被打断,直接返回");            
            return;        
        }        
        try {            
            System.out.println("获取到锁");        
        } finally {            
            lock.unlock();        
        }    
    }, "t1");    
    lock.lock();    
    t1.start();    
    Thread.sleep(2000);    
    System.out.println("主线程进行打断锁");    
    t1.interrupt();
}
实现原理
  • 不可打断模式:即使它被打断,仍会驻留在 AQS 阻塞队列中,一直要等到获得锁后才能得知自己被打断

    java
    public final void acquire(int arg) {    
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待        
            // 如果 acquireQueued 返回 true,打断状态 interrupted = true        
            selfInterrupt();
    }
    static void selfInterrupt() {
        // 知道自己被打断了,需要重新产生一次中断完成中断效果
        Thread.currentThread().interrupt();
    }
    java
    final boolean acquireQueued(final Node node, int arg) {    
        try {        
            boolean interrupted = false;        
            for (;;) {            
                final Node p = node.predecessor();            
                if (p == head && tryAcquire(arg)) {                
                    setHead(node);                
                    p.next = null; // help GC                
                    failed = false;                
                    // 还是需要获得锁后,才能返回打断状态
                    return interrupted;            
                }            
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
                    // 条件二中判断当前线程是否被打断,被打断返回 true,设置中断标记为 true,【获取锁后返回】
                    interrupted = true;  
                }                  
            } 
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    private final boolean parkAndCheckInterrupt() {    
        // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);    
        // 判断当前线程是否被打断,清除打断标记,被打断返回 true
        return Thread.interrupted();
    }
  • 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly,被打断后会直接抛出异常

    java
    public void lockInterruptibly() throws InterruptedException {    
        sync.acquireInterruptibly(1);
    }
    public final void acquireInterruptibly(int arg) {
        // 被其他线程打断了直接返回 false
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            // 没获取到锁,进入这里
            doAcquireInterruptibly(arg);
    }
    java
    private void doAcquireInterruptibly(int arg) throws InterruptedException {
        // 返回封装当前线程的节点
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                //...
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    // 【在 park 过程中如果被 interrupt 会抛出异常】, 而不会再次进入循环获取锁后才完成打断效果
                    throw new InterruptedException();
            }    
        } finally {
            // 抛出异常前会进入这里
            if (failed)
                // 取消当前线程的节点
                cancelAcquire(node);
        }
    }
    java
    // 取消节点出队的逻辑
    private void cancelAcquire(Node node) {
        // 判空
        if (node == null)
            return;
        // 把当前节点封装的 Thread 置为空
        node.thread = null;
        // 获取当前取消的 node 的前驱节点
        Node pred = node.prev;
        // 前驱节点也被取消了,循环找到前面最近的没被取消的节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        
        // 获取前驱节点的后继节点,可能是当前 node,也可能是 waitStatus > 0 的节点
        Node predNext = pred.next;
        
        // 把当前节点的状态设置为【取消状态 1】
        node.waitStatus = Node.CANCELLED;
        
        // 条件成立说明当前节点是尾节点,把当前节点的前驱节点设置为尾节点
        if (node == tail && compareAndSetTail(node, pred)) {
            // 把前驱节点的后继节点置空,这里直接把所有的取消节点出队
            compareAndSetNext(pred, predNext, null);
        } else {
            // 说明当前节点不是 tail 节点
            int ws;
            // 条件一成立说明当前节点不是 head.next 节点
            if (pred != head &&
                // 判断前驱节点的状态是不是 -1,不成立说明前驱状态可能是 0 或者刚被其他线程取消排队了
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                // 如果状态不是 -1,设置前驱节点的状态为 -1
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                // 前驱节点的线程不为 null
                pred.thread != null) {
                
                Node next = node.next;
                // 当前节点的后继节点是正常节点
                if (next != null && next.waitStatus <= 0)
                    // 把 前驱节点的后继节点 设置为 当前节点的后继节点,【从队列中删除了当前节点】
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 当前节点是 head.next 节点,唤醒当前节点的后继节点
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

锁超时

基本使用

public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列

public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出

注意:tryLock 期间也可以被打断

java
public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
        try {
            if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                System.out.println("获取不到锁");
                return;
            }
        } catch (InterruptedException e) {
            System.out.println("被打断,获取不到锁");
            return;
        }
        try {
            log.debug("获取到锁");
        } finally {
            lock.unlock();
        }
    }, "t1");
    lock.lock();
    System.out.println("主线程获取到锁");
    t1.start();
    
    Thread.sleep(1000);
    try {
        System.out.println("主线程释放了锁");
    } finally {
        lock.unlock();
    }
}
实现原理
  • 成员变量:指定超时限制的阈值,小于该值的线程不会被挂起

    java
    static final long spinForTimeoutThreshold = 1000L;

    超时时间设置的小于该值,就会被禁止挂起,因为阻塞在唤醒的成本太高,不如选择自旋空转

  • tryLock()

    java
    public boolean tryLock() {   
        // 只尝试一次
        return sync.nonfairTryAcquire(1);
    }
  • tryLock(long timeout, TimeUnit unit)

    java
    public final boolean tryAcquireNanos(int arg, long nanosTimeout) {
        if (Thread.interrupted())        
            throw new InterruptedException();    
        // tryAcquire 尝试一次
        return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
    }
    protected final boolean tryAcquire(int acquires) {    
        return nonfairTryAcquire(acquires);
    }
    java
    private boolean doAcquireNanos(int arg, long nanosTimeout) {    
        if (nanosTimeout <= 0L)
            return false;
        // 获取最后期限的时间戳
        final long deadline = System.nanoTime() + nanosTimeout;
        //...
        try {
            for (;;) {
                //...
                // 计算还需等待的时间
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L) //时间已到     
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果 nanosTimeout 大于该值,才有阻塞的意义,否则直接自旋会好点
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                // 【被打断会报异常】
                if (Thread.interrupted())
                    throw new InterruptedException();
            }    
        }
    }
哲学家就餐
java
public static void main(String[] args) {
    Chopstick c1 = new Chopstick("1");//...
    Chopstick c5 = new Chopstick("5");
    new Philosopher("苏格拉底", c1, c2).start();
    new Philosopher("柏拉图", c2, c3).start();
    new Philosopher("亚里士多德", c3, c4).start();
    new Philosopher("赫拉克利特", c4, c5).start();    
    new Philosopher("阿基米德", c5, c1).start();
}
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            System.out.println("eating...");
                            Thread.sleep(1000);
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }
}
class Chopstick extends ReentrantLock {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

条件变量

基本使用

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 后继续执行

java
public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();
    //创建一个新的条件变量
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    new Thread(() -> {
        try {
            lock.lock();
            System.out.println("进入等待");
            //进入休息室等待
            condition1.await();
            System.out.println("被唤醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }    
    }).start();
    Thread.sleep(1000);
    //叫醒
    new Thread(() -> {
        try {            
            lock.lock();
            //唤醒
            condition2.signal();
        } finally {
            lock.unlock();
        }
    }).start();
}
实现原理
await

总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,每个 Condition 对象都包含一个等待队列

  • 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里不存在并发

    java
    public final void await() throws InterruptedException {
        // 判断当前线程是否是中断状态,是就直接给个中断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 将调用 await 的线程包装成 Node,添加到条件队列并返回
        Node node = addConditionWaiter();
        // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】
        int savedState = fullyRelease(node);
        
        // 设置打断模式为没有被打断,状态码为 0
        int interruptMode = 0;
        
        // 如果该节点还没有转移至 AQS 阻塞队列,park 阻塞,等待进入阻塞队列
        while (!isOnSyncQueue(node)) {
            LockSupport.park(this);
            // 如果被打断,退出等待队列,对应的 node【也会被迁移到阻塞队列】尾部,状态设置为 0
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】
        
        // 尝试枪锁,释放了多少锁就【重新获取多少锁】,获取锁成功判断打断模式
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        
        // node 在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设 nextWaiter = null
        if (node.nextWaiter != null)
            // 清理条件队列内所有已取消的 Node
            unlinkCancelledWaiters();
        // 条件成立说明挂起期间发生过中断
        if (interruptMode != 0)
            // 应用打断模式
            reportInterruptAfterWait(interruptMode);
    }
    java
    // 打断模式 - 在退出等待时重新设置打断状态
    private static final int REINTERRUPT = 1;
    // 打断模式 - 在退出等待时抛出异常
    private static final int THROW_IE = -1;

  • 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

    java
    private Node addConditionWaiter() {
        // 获取当前条件队列的尾节点的引用,保存到局部变量 t 中
        Node t = lastWaiter;
        // 当前队列中不是空,并且节点的状态不是 CONDITION(-2),说明当前节点发生了中断
        if (t != null && t.waitStatus != Node.CONDITION) {
            // 清理条件队列内所有已取消的 Node
            unlinkCancelledWaiters();
            // 清理完成重新获取 尾节点 的引用
            t = lastWaiter;
        }
        // 创建一个关联当前线程的新 node, 设置状态为 CONDITION(-2),添加至队列尾部
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        if (t == null)
            firstWaiter = node;     // 空队列直接放在队首【不用 CAS 因为执行线程是持锁线程,并发安全】
        else
            t.nextWaiter = node;    // 非空队列队尾追加
        lastWaiter = node;          // 更新队尾的引用
        return node;
    }
    java
    // 清理条件队列内所有已取消(不是 CONDITION)的 node,【链表删除的逻辑】
    private void unlinkCancelledWaiters() {
        // 从头节点开始遍历【FIFO】
        Node t = firstWaiter;
        // 指向正常的 CONDITION 节点
        Node trail = null;
        // 等待队列不空
        while (t != null) {
            // 获取当前节点的后继节点
            Node next = t.nextWaiter;
            // 判断 t 节点是不是 CONDITION 节点,条件队列内不是 CONDITION 就不是正常的
            if (t.waitStatus != Node.CONDITION) { 
                // 不是正常节点,需要 t 与下一个节点断开
                t.nextWaiter = null;
                // 条件成立说明遍历到的节点还未碰到过正常节点
                if (trail == null)
                    // 更新 firstWaiter 指针为下个节点
                    firstWaiter = next;
                else
                    // 让上一个正常节点指向 当前取消节点的 下一个节点,【删除非正常的节点】
                    trail.nextWaiter = next;
                // t 是尾节点了,更新 lastWaiter 指向最后一个正常节点
                if (next == null)
                    lastWaiter = trail;
            } else {
                // trail 指向的是正常节点 
                trail = t;
            }
            // 把 t.next 赋值给 t,循环遍历
            t = next; 
        }
    }
  • 接下来 Thread-0 进入 AQS 的 fullyRelease 流程,释放同步器上的锁

    java
    // 线程可能重入,需要将 state 全部释放
    final int fullyRelease(Node node) {
        // 完全释放锁是否成功,false 代表成功
        boolean failed = true;
        try {
            // 获取当前线程所持有的 state 值总数
            int savedState = getState();
            // release -> tryRelease 解锁重入锁
            if (release(savedState)) {
                // 释放成功
                failed = false;
                // 返回解锁的深度
                return savedState;
            } else {
                // 解锁失败抛出异常
                throw new IllegalMonitorStateException();
            }
        } finally {
            // 没有释放成功,将当前 node 设置为取消状态
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
  • fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功

  • Thread-0 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列,没有就 park 阻塞 Thread-0

    java
    final boolean isOnSyncQueue(Node node) {
        // node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还【没有完成迁移】
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null
        if (node.next != null)
            return true;
        // 说明【可能在阻塞队列,但是是尾节点】
        // 从阻塞队列的尾节点开始向前【遍历查找 node】,如果查找到返回 true,查找不到返回 false
        return findNodeFromTail(node);
    }
  • await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断:在条件队列被打断的线程需要抛出异常

    java
    private int checkInterruptWhileWaiting(Node node) {
        // Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false
        // 如果被中断了,根据是否在条件队列被中断的,设置中断状态码
        return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
    }
    java
    // 这个方法只有在线程是被打断唤醒时才会调用
    final boolean transferAfterCancelledWait(Node node) {
        // 条件成立说明当前 node 一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            // 把【中断唤醒的 node 加入到阻塞队列中】
            enq(node);
            // 表示是在条件队列内被中断了,设置为 THROW_IE 为 -1
            return true;
        }
    
        //执行到这里的情况:
        //1.当前 node 已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了
        //2.当前 node 正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态
        
        // 如果当前线程还没到阻塞队列,一直释放 CPU
        while (!isOnSyncQueue(node))
            Thread.yield();
    
        // 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 为 1
        return false;
    }
  • 最后开始处理中断状态:

    java
    private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
        // 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】
        if (interruptMode == THROW_IE)
            throw new InterruptedException();
    
        // 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】
        else if (interruptMode == REINTERRUPT)
            // 进行一次自己打断,产生中断的效果
            selfInterrupt();
    }
signal
  • 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒,因此 doSignal 内线程安全

    java
    public final void signal() {
        // 判断调用 signal 方法的线程是否是独占锁持有线程
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 获取条件队列中第一个 Node
        Node first = firstWaiter;
        // 不为空就将第该节点【迁移到阻塞队列】
        if (first != null)
            doSignal(first);
    }
    java
    // 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】
    private void doSignal(Node first) {
        do {
            // 成立说明当前节点的下一个节点是 null,当前节点是尾节点了,队列中只有当前一个节点了
            if ((firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        // 将等待队列中的 Node 转移至 AQS 队列,不成功且还有节点则继续循环
        } while (!transferForSignal(first) && (first = firstWaiter) != null);
    }
    
    // signalAll() 会调用这个函数,唤醒所有的节点
    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            Node next = first.nextWaiter;
            first.nextWaiter = null;
            transferForSignal(first);
            first = next;
        // 唤醒所有的节点,都放到阻塞队列中
        } while (first != null);
    }
  • 执行 transferForSignal,先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部,将 Thread-3 的 waitStatus 改为 -1

    java
    // 如果节点状态是取消,返回 false 表示转移失败,否则转移成功
    final boolean transferForSignal(Node node) {
        // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了
        // 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire)
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            // 返回函数调用处继续寻找下一个节点
            return false;
        
        // 【先改状态,再进行迁移】
        // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的【前驱节点】
        Node p = enq(node);
        int ws = p.waitStatus;
        
        // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL,就 unpark 取消当前节点线程的阻塞状态,
        // 让 thread-0 线程竞争锁,重新同步状态
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

  • Thread-1 释放锁,进入 unlock 流程

ReadWrite

读写锁

独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁

共享锁:指该锁可以被多个线程锁持有

ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁

作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写

使用规则:

  • 加锁解锁格式:

    java
    r.lock();
    try {
        // 临界区
    } finally {
        r.unlock();
    }
  • 读 - 读能共存、读 - 写不能共存、写 - 写不能共存

  • 读锁不支持条件变量

  • 重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写

  • 重入时降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁

    java
    w.lock();
    try {
        r.lock();// 降级为读锁,释放写锁,这样能够让其它线程读取缓存
        try {
            // ...
        } finally{
            w.unlock();// 要在写锁释放之前获取读锁
        }
    } finally{
        r.unlock();
    }

构造方法:

  • public ReentrantReadWriteLock():默认构造方法,非公平锁
  • public ReentrantReadWriteLock(boolean fair):true 为公平锁

常用 API:

  • public ReentrantReadWriteLock.ReadLock readLock():返回读锁
  • public ReentrantReadWriteLock.WriteLock writeLock():返回写锁
  • public void lock():加锁
  • public void unlock():解锁
  • public boolean tryLock():尝试获取锁

读读并发:

java
public static void main(String[] args) {
    ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock r = rw.readLock();
    ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    new Thread(() -> {
        r.lock();
        try {
            Thread.sleep(2000);
            System.out.println("Thread 1 running " + new Date());
        } finally {
            r.unlock();
        }
    },"t1").start();
    new Thread(() -> {
        r.lock();
        try {
            Thread.sleep(2000);
            System.out.println("Thread 2 running " + new Date());
        } finally {
            r.unlock();
        }
    },"t2").start();
}

缓存应用

缓存更新时,是先清缓存还是先更新数据库

  • 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存

  • 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据

  • 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

可以使用读写锁进行操作

实现原理

成员属性

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位

  • 读写锁:

    java
    private final ReentrantReadWriteLock.ReadLock readerLock;        
    private final ReentrantReadWriteLock.WriteLock writerLock;
  • 构造方法:默认是非公平锁,可以指定参数创建公平锁

    java
    public ReentrantReadWriteLock(boolean fair) {
        // true 为公平锁
        sync = fair ? new FairSync() : new NonfairSync();
        // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

Sync 类的属性:

  • 统计变量:

    java
    // 用来移位
    static final int SHARED_SHIFT   = 16;
    // 高 16 位的 1
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // 65535,16 个 1,代表写锁的最大重入次数
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // 低 16 位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  • 获取读写锁的次数:

    java
    // 获取读写锁的读锁分配的总次数
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 写锁(独占)锁的重入次数
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • 内部类:

    java
    // 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高 16 位记录的是全局范围内所有的读线程获取读锁的总量
    static final class HoldCounter {
        int count = 0;
        // Use id, not reference, to avoid garbage retention
        final long tid = getThreadId(Thread.currentThread());
    }
    // 线程安全的存放线程各自的 HoldCounter 对象
    static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
  • 内部类实例:

    java
    // 当前线程持有的可重入读锁的数量,计数为 0 时删除
    private transient ThreadLocalHoldCounter readHolds;
    // 记录最后一个获取【读锁】线程的 HoldCounter 对象
    private transient HoldCounter cachedHoldCounter;
  • 首次获取锁:

    java
    // 第一个获取读锁的线程
    private transient Thread firstReader = null;
    // 记录该线程持有的读锁次数(读锁重入次数)
    private transient int firstReaderHoldCount;
  • Sync 构造方法:

    java
    Sync() {
        readHolds = new ThreadLocalHoldCounter();
        // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】
        setState(getState()); 
    }
加锁原理
  • t1 线程:w.lock(写锁),成功上锁 state = 0_1

    java
    // lock()  -> sync.acquire(1);
    public void lock() {
        sync.acquire(1);
    }
    public final void acquire(int arg) {
        // 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上,模式为独占模式 
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    java
    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();
        // 获得低 16 位,代表写锁的 state 计数
        int w = exclusiveCount(c);
        // 说明有读锁或者写锁
        if (c != 0) {
            // c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false
            // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            
            // 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // 写锁重入,获得锁成功,没有并发,所以不使用 CAS
            setState(c + acquires);
            return true;
        }
        
        // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false
        if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
            return false;
        // 获得锁成功,设置锁的持有线程为当前线程
        setExclusiveOwnerThread(current);
        return true;
    }
    // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
    final boolean writerShouldBlock() {
        return false; 
    }
    // 公平锁会检查 AQS 队列中是否有前驱节点,没有 (false) 才去竞争
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
  • t2 r.lock(读锁),进入 tryAcquireShared 流程:

    • 返回 -1 表示失败

    • 如果返回 0 表示成功

    • 返回正数表示还有多少后继节点支持共享模式,读写锁返回 1

      java
      public void lock() {
          sync.acquireShared(1);
      }
      public final void acquireShared(int arg) {
          // tryAcquireShared 返回负数,表示获取读锁失败
          if (tryAcquireShared(arg) < 0)
              doAcquireShared(arg);
      }
      java
      // 尝试以共享模式获取
      protected final int tryAcquireShared(int unused) {
          Thread current = Thread.currentThread();
          int c = getState();
          // exclusiveCount(c) 代表低 16 位,写锁的 state,成立说明有线程持有写锁
          // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】
          if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
              return -1;
          
          // 高 16 位,代表读锁的 state,共享锁分配出去的总次数
          int r = sharedCount(c);
          // 读锁是否应该阻塞
          if (!readerShouldBlock() &&    r < MAX_COUNT &&
              compareAndSetState(c, c + SHARED_UNIT)) {    // 尝试增加读锁计数
              // 加锁成功
              // 加锁之前读锁为 0,说明当前线程是第一个读锁线程
              if (r == 0) {
                  firstReader = current;
                  firstReaderHoldCount = 1;
              // 第一个读锁线程是自己就发生了读锁重入
              } else if (firstReader == current) {
                  firstReaderHoldCount++;
              } else {
                  // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程
                  HoldCounter rh = cachedHoldCounter;
                  // 说明还没设置 rh
                  if (rh == null || rh.tid != getThreadId(current))
                      // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter
                      cachedHoldCounter = rh = readHolds.get();
                  // 还没重入
                  else if (rh.count == 0)
                      readHolds.set(rh);
                  // 重入 + 1
                  rh.count++;
              }
              // 读锁加锁成功
              return 1;
          }
          // 逻辑到这 应该阻塞,或者 cas 加锁失败
          // 会不断尝试 for (;;) 获取读锁,执行过程中无阻塞
          return fullTryAcquireShared(current);
      }
      // 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞
      // 防止一直有读锁线程,导致写锁线程饥饿
      // true 则该阻塞,false 则不阻塞
      final boolean readerShouldBlock() {
          return apparentlyFirstQueuedIsExclusive();
      }
      final boolean readerShouldBlock() {
          return hasQueuedPredecessors();
      }
      java
      final int fullTryAcquireShared(Thread current) {
          // 当前读锁线程持有的读锁次数对象
          HoldCounter rh = null;
          for (;;) {
              int c = getState();
              // 说明有线程持有写锁
              if (exclusiveCount(c) != 0) {
                  // 写锁不是自己则获取锁失败
                  if (getExclusiveOwnerThread() != current)
                      return -1;
              } else if (readerShouldBlock()) {
                  // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入
                  if (firstReader == current) {
                      // assert firstReaderHoldCount > 0;
                  } else {
                      if (rh == null) {
                          // 最后一个读锁的 HoldCounter
                          rh = cachedHoldCounter;
                          // 说明当前线程也不是最后一个读锁
                          if (rh == null || rh.tid != getThreadId(current)) {
                              // 获取当前线程的 HoldCounter
                              rh = readHolds.get();
                              // 条件成立说明 HoldCounter 对象是上一步代码新建的
                              // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队
                              if (rh.count == 0)
                                  // 防止内存泄漏
                                  readHolds.remove();
                          }
                      }
                      if (rh.count == 0)
                          return -1;
                  }
              }
              // 越界判断
              if (sharedCount(c) == MAX_COUNT)
                  throw new Error("Maximum lock count exceeded");
              // 读锁加锁,条件内的逻辑与 tryAcquireShared 相同
              if (compareAndSetState(c, c + SHARED_UNIT)) {
                  if (sharedCount(c) == 0) {
                      firstReader = current;
                      firstReaderHoldCount = 1;
                  } else if (firstReader == current) {
                      firstReaderHoldCount++;
                  } else {
                      if (rh == null)
                          rh = cachedHoldCounter;
                      if (rh == null || rh.tid != getThreadId(current))
                          rh = readHolds.get();
                      else if (rh.count == 0)
                          readHolds.set(rh);
                      rh.count++;
                      cachedHoldCounter = rh; // cache for release
                  }
                  return 1;
              }
          }
      }
  • 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态

    java
    private void doAcquireShared(int arg) {
        // 将当前线程关联到一个 Node 对象上,模式为共享模式
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点就头节点就去尝试获取锁
                if (p == head) {
                    // 再一次尝试获取读锁
                    int r = tryAcquireShared(arg);
                    // r >= 0 表示获取成功
                    if (r >= 0) {
                        //【这里会设置自己为头节点,唤醒相连的后序的共享节点】
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 是否在获取读锁失败时阻塞                           park 当前线程
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park

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

解锁原理
  • t1 w.unlock,写锁解锁

    java
    public void unlock() {
        // 释放锁
        sync.release(1);
    }
    public final boolean release(int arg) {
        // 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        // 因为可重入的原因,写锁计数为 0, 才算释放成功
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }
  • 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一

  • 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,唤醒连续的所有的共享节点

    java
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; 
        // 设置自己为 head 节点
        setHead(node);
        // propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 获取下一个节点
            Node s = node.next;
            // 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】
            if (s == null || s.isShared())
                // 唤醒后继节点
                doReleaseShared();
        }
    }
    java
    private void doReleaseShared() {
        // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
        // 如果 head.waitStatus == 0 ==> Node.PROPAGATE
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // SIGNAL 唤醒后继
                if (ws == Node.SIGNAL) {
                    // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
                    // 防止 unparkSuccessor 被多次执行
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;  
                    // 唤醒后继节点
                    unparkSuccessor(h);
                }
                // 如果已经是 0 了,改为 -3,用来解决传播性
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head,
            // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
            if (h == head)                   
                break;
        }
    }
  • 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

  • t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点

    java
    public void unlock() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    java
    protected final boolean tryReleaseShared(int unused) {
    
        for (;;) {
            int c = getState();
            int nextc = c - SHARED_UNIT;
            // 读锁的计数不会影响其它获取读锁线程,但会影响其它获取写锁线程,计数为 0 才是真正释放
            if (compareAndSetState(c, nextc))
                // 返回是否已经完全释放了 
                return nextc == 0;
        }
    }
  • t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束

Stamped

StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能

特点:

  • 在使用读锁、写锁时都必须配合戳使用

  • StampedLock 不支持条件变量

  • StampedLock 不支持重入

基本用法

  • 加解读锁:

    java
    long stamp = lock.readLock();
    lock.unlockRead(stamp);            // 类似于 unpark,解指定的锁
  • 加解写锁:

    java
    long stamp = lock.writeLock();
    lock.unlockWrite(stamp);
  • 乐观读,StampedLock 支持 tryOptimisticRead() 方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性

    java
    long stamp = lock.tryOptimisticRead();
    // 验戳
    if(!lock.validate(stamp)){
        // 锁升级
    }

提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法:

  • 读 - 读可以优化
  • 读 - 写优化读,补加读锁
java
public static void main(String[] args) throws InterruptedException {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> {
        dataContainer.read(1000);
    },"t1").start();
    Thread.sleep(500);
    
    new Thread(() -> {
        dataContainer.write(1000);
    },"t2").start();
}

class DataContainerStamped {
    private int data;
    private final StampedLock lock = new StampedLock();

    public int read(int readTime) throws InterruptedException {
        long stamp = lock.tryOptimisticRead();
        System.out.println(new Date() + " optimistic read locking" + stamp);
        Thread.sleep(readTime);
        // 戳有效,直接返回数据
        if (lock.validate(stamp)) {
            Sout(new Date() + " optimistic read finish..." + stamp);
            return data;
        }

        // 说明其他线程更改了戳,需要锁升级了,从乐观读升级到读锁
        System.out.println(new Date() + " updating to read lock" + stamp);
        try {
            stamp = lock.readLock();
            System.out.println(new Date() + " read lock" + stamp);
            Thread.sleep(readTime);
            System.out.println(new Date() + " read finish..." + stamp);
            return data;
        } finally {
            System.out.println(new Date() + " read unlock " +  stamp);
            lock.unlockRead(stamp);
        }
    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        System.out.println(new Date() + " write lock " + stamp);
        try {
            Thread.sleep(2000);
            this.data = newData;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(new Date() + " write unlock " + stamp);
            lock.unlockWrite(stamp);
        }
    }
}

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() 方法之间的代码为同步代码
java
public static void main(String[] args) {
    // 1.创建 Semaphore 对象
    Semaphore semaphore = new Semaphore(3);

    // 2. 10 个线程同时运行
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                // 3. 获取许可
                semaphore.acquire();
                sout(Thread.currentThread().getName() + " running...");
                Thread.sleep(1000);
                sout(Thread.currentThread().getName() + " end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 4. 释放许可
                semaphore.release();
            }
        }).start();
    }
}

实现原理

加锁流程:

  • Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源

    java
    Sync(int permits) {
        setState(permits);
    }

    假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞

    java
    // acquire() -> sync.acquireSharedInterruptibly(1),可中断
    public final void acquireSharedInterruptibly(int arg) {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取通行证,获取成功返回 >= 0 的值
        if (tryAcquireShared(arg) < 0)
            // 获取许可证失败,进入阻塞
            doAcquireSharedInterruptibly(arg);
    }
    
    // tryAcquireShared() -> nonfairTryAcquireShared()
    // 非公平,公平锁会在循环内 hasQueuedPredecessors() 方法判断阻塞队列是否有临头节点 (第二个节点)
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            // 获取 state,state 这里【表示通行证】
            int available = getState();
            // 计算当前线程获取通行证完成之后,通行证还剩余数量
            int remaining = available - acquires;
            // 如果许可已经用完,返回负数,表示获取失败,
            if (remaining < 0 ||
                // 许可证足够分配的,如果 cas 重试成功,返回正数,表示获取成功
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
    java
    private void doAcquireSharedInterruptibly(int arg) {
        // 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中
        final Node node = addWaiter(Node.SHARED);
        // 获取标记
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                // 前驱节点是头节点可以再次获取许可
                if (p == head) {
                    // 再次尝试获取许可,【返回剩余的许可证数量】
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 成功后本线程出队(AQS), 所在 Node 设置为 head
                        // r 表示【可用资源数】, 为 0 则不会继续传播
                        setHeadAndPropagate(node, r); 
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 不成功,设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            // 被打断后进入该逻辑
            if (failed)
                cancelAcquire(node);
        }
    }
    java
    private void setHeadAndPropagate(Node node, int propagate) {    
        Node h = head;
        // 设置自己为 head 节点
        setHead(node);
        // propagate 表示有【共享资源】(例如共享读锁或信号量)
        // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

  • 这时 Thread-4 释放了 permits,状态如下

    java
    // release() -> releaseShared()
    public final boolean releaseShared(int arg) {
        // 尝试释放锁
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }    
        return false;
    }
    protected final boolean tryReleaseShared(int releases) {    
        for (;;) {
            // 获取当前锁资源的可用许可证数量
            int current = getState();
            int next = current + releases;
            // 索引越界判断
            if (next < current)            
                throw new Error("Maximum permit count exceeded");        
            // 释放锁
            if (compareAndSetState(current, next))            
                return true;    
        }
    }
    private void doReleaseShared() {    
        // PROPAGATE 详解    
        // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
        // 如果 head.waitStatus == 0 ==> Node.PROPAGATE
    }

  • 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态

PROPAGATE

假设存在某次循环中队列里排队的结点情况为 head(-1) → t1(-1) → t2(0),存在将要释放信号量的 T3 和 T4,释放顺序为先 T3 后 T4

java
// 老版本代码
private void setHeadAndPropagate(Node node, int propagate) {    
    setHead(node);    
    // 有空闲资源    
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;        
        // 下一个        
        if (s == null || s.isShared())            
            unparkSuccessor(node);        
    }
}

正常流程:

  • T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
  • T1 由于 T3 释放信号量被唤醒,然后 T4 释放,唤醒 T2

BUG 流程:

  • T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
  • T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量)
  • T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),不满足条件,因此不调用 unparkSuccessor(head)
  • T1 获取信号量成功,调用 setHeadAndPropagate(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, T2 线程得不到唤醒

更新后流程:

  • T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0

  • T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量)

  • T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)

  • T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2

java
private void setHeadAndPropagate(Node node, int propagate) {    
    Node h = head;
    // 设置自己为 head 节点
    setHead(node);
    // propagate 表示有共享资源(例如共享读锁或信号量)
    // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
java
// 唤醒
private void doReleaseShared() {
    // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
    // 如果 head.waitStatus == 0 ==> Node.PROPAGATE    
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 防止 unparkSuccessor 被多次执行
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果已经是 0 了,改为 -3,用来解决传播性
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}

CountDown

基本使用

CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成

构造器:

  • public CountDownLatch(int count):初始化唤醒需要的 down 几步

常用 API:

  • public void await():让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待
  • public void countDown():计数器进行减 1(down 1)

应用:同步等待多个 Rest 远程调用结束

java
// LOL 10 人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(10);
    ExecutorService service = Executors.newFixedThreadPool(10);
    String[] all = new String[10];
    Random random = new Random();

    for (int j = 0; j < 10; j++) {
        int finalJ = j;//常量
        service.submit(() -> {
            for (int i = 0; i <= 100; i++) {
                Thread.sleep(random.nextInt(100));  //随机休眠
                all[finalJ] = i + "%";
                System.out.print("\r" + Arrays.toString(all));  // \r代表覆盖
            }
            latch.countDown();
        });
    }
    latch.await();
    System.out.println("\n游戏开始");
    service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始

实现原理

阻塞等待:

  • 线程调用 await() 等待其他线程完成任务:支持打断

    java
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    // AbstractQueuedSynchronizer#acquireSharedInterruptibly
    public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        // 判断线程是否被打断,抛出打断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源
        // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    // CountDownLatch.Sync#tryAcquireShared
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }
  • 线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0:

    java
    private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
        // 将调用 latch.await() 方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                // 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 前驱节点时头节点就可以尝试获取锁
                if (p == head) {
                    // 再次尝试获取锁,获取成功返回 1
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 获取锁成功,设置当前节点为 head 节点,并且向后传播
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 阻塞在这里
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            // 阻塞线程被中断后抛出异常,进入取消节点的逻辑
            if (failed)
                cancelAcquire(node);
        }
    }
  • 获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点:

    java
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        // 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null
        setHead(node);
        // propagate = 1,条件一成立
        if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
            // 获取当前节点的后继节点
            Node s = node.next;
            // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式
            if (s == null || s.isShared())
                // 唤醒所有的等待共享锁的节点
                doReleaseShared();
        }
    }

计数减一:

  • 线程进入 countDown() 完成计数器减一(释放锁)的操作

    java
    public void countDown() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        // 尝试释放共享锁
        if (tryReleaseShared(arg)) {
            // 释放锁成功开始唤醒阻塞节点
            doReleaseShared();
            return true;
        }
        return false;
    }
  • 更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true

    java
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int c = getState();
            // 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false
            if (c == 0)
                return false;
            // 计数器减一
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                // 计数器为 0 时返回 true
                return nextc == 0;
        }
    }
  • state = 0 时,当前线程需要执行唤醒阻塞节点的任务

    java
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            // 判断队列是否是空队列
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 头节点的状态为 signal,说明后继节点没有被唤醒过
                if (ws == Node.SIGNAL) {
                    // cas 设置头节点的状态为 0,设置失败继续自旋
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    // 唤醒后继节点
                    unparkSuccessor(h);
                }
                // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;
            }
            // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head,
            // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
            if (h == head)
                break;
        }
    }

CyclicBarrier

基本使用

CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行

常用方法:

  • public CyclicBarrier(int parties, Runnable barrierAction):用于在线程到达屏障 parties 时,执行 barrierAction
    • parties:代表多少个线程到达屏障开始触发线程任务
    • barrierAction:线程任务
  • public int await():线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障

与 CountDownLatch 的区别:CyclicBarrier 是可以重用的

应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发

java
public static void main(String[] args) {
    ExecutorService service = Executors.newFixedThreadPool(2);
    CyclicBarrier barrier = new CyclicBarrier(2, () -> {
        System.out.println("task1 task2 finish...");
    });

    for (int i = 0; i < 3; i++) { // 循环重用
        service.submit(() -> {
            System.out.println("task1 begin...");
            try {
                Thread.sleep(1000);
                barrier.await();    // 2 - 1 = 1
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        service.submit(() -> {
            System.out.println("task2 begin...");
            try {
                Thread.sleep(2000);
                barrier.await();    // 1 - 1 = 0
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
    }
    service.shutdown();
}

实现原理

成员属性
  • 全局锁:利用可重入锁实现的工具类

    java
    // barrier 实现是依赖于 Condition 条件队列,condition 条件队列必须依赖 lock 才能使用
    private final ReentrantLock lock = new ReentrantLock();
    // 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒
    private final Condition trip = lock.newCondition();
  • 线程数量:

    java
    private final int parties;    // 代表多少个线程到达屏障开始触发线程任务
    private int count;            // 表示当前“代”还有多少个线程未到位,初始值为 parties
  • 当前代中最后一个线程到位后要执行的事件:

    java
    private final Runnable barrierCommand;
  • 代:

    java
    // 表示 barrier 对象当前 代
    private Generation generation = new Generation();
    private static class Generation {
        // 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常
        // 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。
        boolean broken = false;
    }
  • 构造方法:

    java
    public CyclicBarrie(int parties, Runnable barrierAction) {
        // 因为小于等于 0 的 barrier 没有任何意义
        if (parties <= 0) throw new IllegalArgumentException();
    
        this.parties = parties;
        this.count = parties;
        // 可以为 null
        this.barrierCommand = barrierAction;
    }
成员方法
  • await():阻塞等待所有线程到位

    java
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
    java
    // timed:表示当前调用 await 方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的
    // nanos:线程等待超时时长,单位是纳秒
    private int dowait(boolean timed, long nanos) {
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lock();
        try {
            // 获取当前代
            final Generation g = generation;
    
            // 【如果当前代是已经被打破状态,则当前调用 await 方法的线程,直接抛出 Broken 异常】
            if (g.broken)
                throw new BrokenBarrierException();
            // 如果当前线程被中断了,则打破当前代,然后当前线程抛出中断异常
            if (Thread.interrupted()) {
                // 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程
                breakBarrier();
                throw new InterruptedException();
            }
    
            // 逻辑到这说明,当前线程中断状态是 false,当前代的 broken 为 false(未打破状态)
            
            // 假设 parties 给的是 5,那么 index 对应的值为 4,3,2,1,0
            int index = --count;
            // 条件成立说明当前线程是最后一个到达 barrier 的线程,【需要开启新代,唤醒阻塞线程】
            if (index == 0) {
                // 栅栏任务启动标记
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        // 启动触发的任务
                        command.run();
                    // run() 未抛出异常的话,启动标记设置为 true
                    ranAction = true;
                    // 开启新的一代,这里会【唤醒所有的阻塞队列】
                    nextGeneration();
                    // 返回 0 因为当前线程是此代最后一个到达的线程,index == 0
                    return 0;
                } finally {
                    // 如果 command.run() 执行抛出异常的话,会进入到这里
                    if (!ranAction)
                        breakBarrier();
                }
            }
    
            // 自旋,一直到条件满足、当前代被打破、线程被中断,等待超时
            for (;;) {
                try {
                    // 根据是否需要超时等待选择阻塞方法
                    if (!timed)
                        // 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    // 被中断后来到这里的逻辑
                    
                    // 当前代没有变化并且没有被打破
                    if (g == generation && !g.broken) {
                        // 打破屏障
                        breakBarrier();
                        // node 节点在【条件队列】内收到中断信号时 会抛出中断异常
                        throw ie;
                    } else {
                        // 等待过程中代变化了,完成一次自我打断
                        Thread.currentThread().interrupt();
                    }
                }
                // 唤醒后的线程,【判断当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常】
                if (g.broken)
                    throw new BrokenBarrierException();
    
                // 当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑
                if (g != generation)
                    return index;
                // 当前线程 trip 中等待超时,然后主动转移到阻塞队列
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    // 抛出超时异常
                    throw new TimeoutException();
                }
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }
  • breakBarrier():打破 Barrier 屏障

    java
    private void breakBarrier() {
        // 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常
        generation.broken = true;
        // 重置 count 为 parties
        count = parties;
        // 将在 trip 条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常
        trip.signalAll();
    }
  • nextGeneration():开启新的下一代

    java
    private void nextGeneration() {
        // 将在 trip 条件队列内挂起的线程全部唤醒
        trip.signalAll();
        // 重置 count 为 parties
        count = parties;
    
        // 开启新的一代,使用一个新的 generation 对象,表示新的一代,新的一代和上一代【没有任何关系】
        generation = new Generation();
    }

Exchanger

Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换

工作流程:两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据

常用方法:

  • public Exchanger():创建一个新的交换器
  • public V exchange(V x):等待另一个线程到达此交换点
  • public V exchange(V x, long timeout, TimeUnit unit):等待一定的时间
java
public class ExchangerDemo {
    public static void main(String[] args) {
        // 创建交换对象(信使)
        Exchanger<String> exchanger = new Exchanger<>();
        new ThreadA(exchanger).start();
        new ThreadB(exchanger).start();
    } 
}
class ThreadA extends Thread{
    private Exchanger<String> exchanger();
    
    public ThreadA(Exchanger<String> exchanger){
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        try{
            sout("线程 A,做好了礼物 A,等待线程 B 送来的礼物 B");
            //如果等待了 5s 还没有交换就死亡(抛出异常)!
            String s = exchanger.exchange("礼物 A",5,TimeUnit.SECONDS);
            sout("线程 A 收到线程 B 的礼物:" + s);
        } catch (Exception e) {
            System.out.println("线程 A 等待了 5s,没有收到礼物,最终就执行结束了!");
        }
    }
}
class ThreadB extends Thread{
    private Exchanger<String> exchanger;
    
    public ThreadB(Exchanger<String> exchanger) {
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        try {
            sout("线程 B,做好了礼物 B,等待线程 A 送来的礼物 A.....");
            // 开始交换礼物。参数是送给其他线程的礼物!
            sout("线程 B 收到线程 A 的礼物:" + exchanger.exchange("礼物 B"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史