☞ JMM 是什么 JMM(Java 内存模型:Java Memory Model,简称 JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。 根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有实例变量都储存在主存中,对于所有线程都是共享的。每个线程都有自己的工作内存(Working Memory)是私有数据区域。线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝。不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
☞ JMM 特性
特性 | 说明 |
---|---|
可见性 | 一个线程对共享变量做了修改之后,其他的线程立即能够感知到该变量的修改。 |
原子性 | 一个操作不能被打断,要么全部执行完毕,要么不执行。 |
有序性 | JMM 允许指令重排,但不管怎么重排,重排后的指令绝对不能改变原有的串行语义。 |
volatile 是 Java 提供的一种轻量级的同步机制,volatile 基本满足了 JMM 要求,保证了可见性、禁止指令重排(有序性)但是不保证原子性。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 资源类
*/
public class MyData{
// 没有 volatile 修饰时,没有可见性
int i = 0;
public void change() {
this.i = 100;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc Volatile 可见性
*/
public class VolatileDemo {
public static void main(String[] args) {
// 资源类
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始操作 ===== MyData");
try {
// 模拟操作耗时
Thread.sleep(300);
myData.change();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 操作完成 ===== MyData");
}, "volatile").start();
while (0 == myData.i) {}
System.out.println(Thread.currentThread().getName() + " 我来康康 i = " + myData.i);
}
}
运行上述代码可以发现,程序一致未结束,很明显是卡在了 while 循环,mian 线程一致认为 MyData 中 的变量 i 的值是 0,所以不会退出循环。现在我们在变量 i 上加 volatile 关键字后再此执行,发现 mian 发现了变量 i 的修改,退出了 while 循环。
☞ 不保证原子性
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 资源类
*/
public class MyData {
volatile int i = 0;
public void add() {
i++;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc Volatile 不保证原子性
*/
public class VolatileDemo {
public static void main(String[] args) {
// 资源类
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.add();
}
}, "add-" + i).start();
}
// mian 与 GC 线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " 我来康康 i = " + myData.i);
}
}
运行上述代码,输出结果并非我们所想为 20000,这是因为 volatile 不保证原子性,会导致在线程执行过程中有线程加塞。例如,add-0
线程读到 i = 1 执行 ++ 操作,add-1
线程也读到 i = 1 执行 ++ 操作,正当二者要写回主内存时,add-0
线程阻塞,add-1
线程将 i = 2 写回了主内存,正准备通知其他线程 i 修改了,add-0
线程先一步将 i = 2 也写回了主内存,就造成了丢失。
☞ 解决方案
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 资源类
*/
public class MyData {
volatile int i = 0;
// 不推荐使用 synchronized,高射炮打蚊子
public synchronized void add() {
i++;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 资源类
*/
public class MyData {
// 推荐使用 atomic 原子操作接口
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
☞ 什么是指令重排 在虚拟机层面,为了尽可能减少内存操作速度远慢于 CPU 运行速度所带来的 CPU 空置的影响,虚拟机会按照自己的一些规则将指令重排——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用 CPU。在硬件层面,与虚拟机层面原因类似,CPU 会将接收到的一批指令按照其规则重排序,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。
上图便是汇编指令的执行过程,在某些指令上存在 X 的标志,X 代表中断的含义,也就是只要有 X 的地方就会导致指令流水线技术停顿,同时也会影响后续指令的执行,可能需要经过 1 个或几个指令周期才可能恢复正常,那为什么停顿呢?这是因为部分数据还没准备好,如执行 ADD 指令时,需要使用到前面指令的数据 R1,R2,而此时 R2 的 MEM 操作没有完成,即未拷贝到存储器中,这样加法计算就无法进行,必须等到 MEM 操作完成后才能执行,也就因此而停顿了,其他指令也是类似的情况。
停顿会造成 CPU 性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了,既然 ADD 指令需要等待,那我们就利用等待的时间做些别的事情,如把 LW R4,e
和 LW R5,f
移动到前面执行,毕竟 LW R4,e
和 LW R5,f
执行并没有数据依赖关系,对他们有数据依赖关系的 SUB R6,R5,R4
指令在 R4,R5
加载完成后才执行的,所以没有影响。重排后,所有的停顿都完美消除了,指令流水线也无需中断了,这样 CPU 的性能也能带来很好的提升,这就是处理器指令重排的作用。
☞ 内存屏障 内存屏障是 CPU 指令。如果你的字段是 volatile,Java 内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。下面是基于保守策略的 JMM 内存屏障插入策略: ♞ 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 ♞ 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 ♞ 在每个 volatile 读操作的前面插入一个 LoadLoad 屏障。 ♞ 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。 内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。AtomicInteger 类中的 compareAndSet 方法就是这种思想。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CASDemo {
public static void main(String[] args) {
// 创建初始值是 5 的原子型整数
AtomicInteger atomicInteger = new AtomicInteger(5);
// 期望当前变量值没有人动过,依旧是 5 时,将 5 替换为 2021
System.out.println(atomicInteger.compareAndSet(5, 2021) + " == " + atomicInteger);
// 期望当前变量值没有人动过,依旧是 5 时,将 5 替换为 2022
System.out.println(atomicInteger.compareAndSet(5, 2022) + " == " + atomicInteger);
}
}
在前文的 volatile 的原子性中我们使用了 AtomicInteger 类中的 getAndIncrement 方法,为什么他就能保证原子性。我们来看一下源码,发现最终在 Unsafe 类中使用了一个 while 循环,首先调用 getIntVolatile 方法根据对象和偏移值获取到内存中的数据,相当于将主存数据复制到本地工作空间,然后调用 compareAndSwapInt 方法来判断期望的值与主存的值是否一致,一致则更新主存值,返回 true 取反退出循环,否则继续循环。
① 循环开销大:底层使用的是 while 循环,极限情况可能导致循环 N 次,性能开销大 ② 只能保证一个共享变量原子操作 ③ ABA 问题:一个线程速度较快,将 A 改为 B 后又改为 A,其他线程一看还是 A 认为没有人动过。
ABA 问题的产生是因为有人改过而我不知道,那么改过之后记录以下不就好了。我们都用过 Git 当我们拉取最新版本的代码,修改了某个地方提交并推送到远端后,觉得修改的有问题,还是原来的好,就把修改的地方恢复了再次提交并推送到远端。这个时候另外一个人肯定知道你改过代码,因为有版本号。同样的 Java 也提供了携带版本号的原子引用类型 AtomicStampedReference<V>
。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CASDemo {
public static void main(String[] args) {
// 携带版本号的原子引用类型
AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<>(5, 1);
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " ☞ 我初次获取的版本号:" + stamp);
// 等待 B 线程获取版本号
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ABA 操作
boolean b = integerAtomicStampedReference.compareAndSet(5, 100, stamp, stamp + 1);
System.out.print(Thread.currentThread().getName() + " ☞ 5 -> 100 为 " + b);
System.out.println(", 版本号:" + integerAtomicStampedReference.getStamp());
boolean b1 = integerAtomicStampedReference.compareAndSet(100, 5, stamp + 1, stamp + 2);
System.out.print(Thread.currentThread().getName() + " ☞ 100 -> 5 为 " + b1);
System.out.println(", 版本号:" + integerAtomicStampedReference.getStamp());
}, "A").start();
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " ☞ 我初次获取的版本号:" + stamp);
// 等待 A 线程完成 ABA 操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = integerAtomicStampedReference.compareAndSet(5, 200, stamp, stamp + 1);
System.out.print(Thread.currentThread().getName() + " ☞ 5 -> 200 为 " + b);
System.out.println(", 版本号:" + integerAtomicStampedReference.getStamp());
}, "B").start();
}
}
一般我们认为锁大体分为两种,乐观锁和悲观锁。乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作,如果失败则要重复读-比较-写的操作,Java 中的乐观锁基本都是通过 CAS 实现的。悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
公平锁(Fair)
:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点是所有的线程都能得到资源,不会饿死在队列中。缺点是吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU 唤醒阻塞线程的开销会很大。
非公平锁(Nonfair)
:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点是可以减少 CPU 唤醒线程的开销,整体的吞吐效率会高点,CPU 也不必取唤醒所有线程,会减少唤起线程的数量。缺点是可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。synchronized 是非公平锁,ReentrantLock 空参构造返回的是非公平锁。若是要创建公平锁则使用 ReentrantLock 有参构造传入 true。
可重入锁,也叫做递归锁,它的可重入性表现在同一个线程可以多次获得锁,指的是同一线程有内外两层被同一把锁锁住的函数,外层函数获得锁之后,进入内层函数时自动获得锁,并且不发生死锁。ReentrantLock 和 synchronized 都是可重入锁。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 可重入锁
*/
public class MyText {
Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 执行外层函数");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
method2();
}
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 执行内层函数");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class ReentrantDemo {
public static void main(String[] args) {
MyText myText = new MyText();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
myText.method1();
}, ((char)(65 + i) + "").start();
}
}
}
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋,循环获取锁),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。 线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 自旋锁
*/
public class MyLock {
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
while (!threadAtomicReference.compareAndSet(null, thread)){}
System.out.println(thread.getName() + " 锁住了");
}
public void myUnLock() {
Thread thread = Thread.currentThread();
threadAtomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + " 开锁了");
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class SpinLockDemo {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(() -> {
lock.myLock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.myUnLock();
}, "A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.myLock();
lock.myUnLock();
}, "B").start();
}
}
独占锁
:独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排他锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。ReentrantLock 和 synchronized 都是独占锁
共享锁
:共享锁是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。ReentrantReadWriteLock 读锁是共享锁,写锁是独占锁。读锁的共享可以保证并发读是高效的,读写,写读,写写是互斥的
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 读写锁
*/
public class MyCache {
private volatile Map<String, String> map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void put(String key, String value) {
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入:" + key);
Thread.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
public void get(String key) {
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取:" + key);
Thread.sleep(300);
map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", "");
}, ((char)(65 + i) + "")).start();
}
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI + "");
}, ((char)(65 + i) + "")).start();
}
}
}
为什么要加读锁? 读锁自然也是为了避免原子性问题,比如一个 long 型参数的写操作并不是原子性的,如果允许同时读和写,那读到的数很可能是就是写操作的中间状态,比如刚写完前 32位,就被读到了。
CountDownLatch 这个类使一个线程等待其他线程各自执行完毕后再执行。内部是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就 -1,当计数器的值为 0 时,表示所有线程都执行完毕,然后等待的线程就可以恢复工作了。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CountDownLatch
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 吃完了");
countDownLatch.countDown();
}, ((char)(65 + i) + "")).start();
}
try {
// 等待线程执行完毕
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 收桌子了");
}
}
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召唤神龙"));
for (int i = 0; i < 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 收集到了龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, ((char)(65 + i) + "")).start();
}
}
}
Semaphore 是计数信号量。Semaphore 管理一系列许可。每个 acquire 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可这个对象,Semaphore 只是维持了一个可获得许可证的数量。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc Semaphore
*/
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 抢到了车位");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 离开了车位");
}
}, ((char)(65 + i) + "")).start();
}
}
}
Exchanger 是 JDK 1.5 开始提供的一个用于两个工作线程之间交换数据的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。其定义为 Exchanger<V>
泛型类型,其中 V 表示可交换的数据类型。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc Exchanger
*/
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> stringExchanger = new Exchanger<>();
new Thread(() -> {
try {
String data = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + " 交换前数据:" + data);
String exchange = stringExchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 交换后数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
String data = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + " 交换前数据:" + data);
String exchange = stringExchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 交换后数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架,Java 中的大部分同步类(Lock、Semaphore、ReentrantLock 等)都是基于 AQS 实现的。AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个 JUC 体系的基石,使用一个 volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成 一个 Node 节点来实现锁的分配,通过 CAS 完成对 State 值的修改。
抢到资源的线程可以直接执行业务逻辑,抢占不到资源的线程的必然要去排队等候,AQS 的工作就是将排队等候安排的明明白白的。AQS 内部维护了一个 CLH 队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个 node 节点加入到同步队列 sync queue里。当前节点为 head 的直接后继节点时就会尝试获取锁。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系。
最开始,一切准备就绪,但是没有任何线程进来,好比银行刚开门没有任何人开办理业务。
第一个线程 ThreadA 开始调用 lock 方法抢占锁,首先判断当前 state 是否是 0 空闲状态,若是则将其设置为 1,然后将当前执行线程设置为 ThreadA。就好比第一个顾客进入银行后独占唯一的一个窗口开始办理业务。
第二个线程 ThreadB 也开始调用 lock 方法抢占锁,发现 state 为 1,有人占了,然后就执行 acquire 方法,acquire 方法调用 tryAcquire 方法,tryAcquire 方法又调用 nonfairTryAcquire 方法,在 nonfairTryAcquire 方法中判断当前 state 是否为 0,不为零在判断当前线程是否是正在执行的线程。此处由于 ThreadA 仍在执行,所以返回 false。
然后调用 addWaiter 方法,在 addWaiter 方法调用 enq 方法,很明显这个方法里面是一个自旋,第一次由于尾指针 tail 是指向 null 的,所以添加一个空的节点,该节点被称为哨兵节点,并将 tail 指向哨兵节点;第二次 tail 非空,则将 tail 指向真正的 ThreadB node,并将哨兵节点的 next 也指向该 node。紧接着执行 acquireQueued 方法,该方法里面又是一个自旋,自旋时当前为哨兵节点后第一个节点时会再次尝试抢占锁,未抢到会调用 shouldParkAfterFailedAcquire 方法,将哨兵节点的 waitStatus 设为 -1 后进行第二次自旋,第二次自旋 shouldParkAfterFailedAcquire 返回 true,开始执行 parkAndCheckInterrupt 方法,该方法让 ThreadB 阻塞。
第三个线程 ThreadC 也开始调用 lock 方法抢占锁,同理将其加入到队列等待。ThreadB、ThreadC 的这种操作类似于,第二个顾客进入银行,看到第一个顾客在办理业务,就走到了候客区,然后再看了一眼第一个顾客,发现还没有办理完,然后坐下了。
ThreadA 执行完毕后,调用 unlock 释放锁,在 unlock 方法中调用 release 方法,在 release 方法中调用 tryRelease 方法,tryRelease 方法将 state 设为 0 并将当前执行线程设置为 null,然后调用 unparkSuccessor 方法,在该方法中将 ThreadB 的 waitStatus 设为 0,然后唤醒 ThreadB。
ThreadB 被唤醒后,返回 false,继续自旋,这个时候由于没有线程占用,ThreadB 直接获取到了锁,然后将当前 ThreadB 节点修改为哨兵节点,原有哨兵节点等待 GC 回收。