第 3 章 JUC
1、题目说明
1、Synchronized 相关问题
- Synchronized 用过吗,其原理是什么?
- 你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁
- 什么是可重入性,为什么说Synchronized 是可重入锁?
- JVM对Java的原生锁做了哪些优化?
- 为什么说Synchronized是非公平锁?
- 什么是锁消除和锁粗化?
- 为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么优点和缺点?
- 乐观锁一定就是好的吗?
2、可重入锁 ReentrantLock及其他显式锁相关问题
- 跟Synchronized相比,可重入锁ReentrantLock 其实现原理有什么不同?
- 那么请谈谈AQS框架是怎么回事儿?
- 请尽可能详尽地对比下Synchronized 和ReentrantLock的异同。
- ReentrantLock 是如何实现可重入性的?
2、可重入锁
2.1、可重入锁概述
可重入锁的概念
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
2.2、可重入锁的字面解释
可重入锁
- 可:可以。
- 重:再次。
- 入:进入
- 锁:同步锁
进入什么?
进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话说清楚
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。即自己可以获取自己的内部锁
2.3、可重入锁种类
2.3.1、隐式锁
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
1、同步代码块
-
代码
/** * @ClassName ReEnterLockDemo * @Description 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的 * @Author Oneby * @Date 2020/12/27 11:10 * @Version 1.0 */ public class ReEnterLockDemo { // synchronized 同步代码块可重入演示 static Object objectLockA = new Object(); public static void m1() { new Thread(() -> { synchronized (objectLockA) { System.out.println(Thread.currentThread().getName() + "\t" + "------外层调用"); synchronized (objectLockA) { System.out.println(Thread.currentThread().getName() + "\t" + "------中层调用"); synchronized (objectLockA) { System.out.println(Thread.currentThread().getName() + "\t" + "------内层调用"); } } } }, "t1").start(); } public static void main(String[] args) { m1(); } }
-
程序运行结果:在同一个线程内部成功获取同一把锁
2、同步方法
-
代码
/** * @ClassName ReEnterLockDemo * @Description 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的 * @Author Oneby * @Date 2020/12/27 11:10 * @Version 1.0 */ public class ReEnterLockDemo { // synchronized 同步方法可重入演示 public synchronized void m1() { System.out.println("=====外层"); m2(); } public synchronized void m2() { System.out.println("=====中层"); m3(); } public synchronized void m3() { System.out.println("=====内层"); } public static void main(String[] args) { new ReEnterLockDemo().m1(); } }
-
程序运行结果
synchronized 原理
-
在 IDEA 终端中进入包名所在的文件夹
-
使用
javap -c xxx.class
指令反编译字节码文件,可以看到有一对配对出现的monitorenter
和monitorexit
指令,一个对应于加锁,一个对应于解锁 -
为什么会多出来一个
monitorexit
指令呢?如果同步代码块中出现Exception或者Error,则会调用第二个
monitorexit
指令来保证释放锁
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter
时,如果目标锋对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit
时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
2.3.2、显示锁
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
代码示例一:可重入演示
-
代码
/** * @ClassName ReEnterLockDemo * @Description 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的 * @Author Oneby * @Date 2020/12/27 11:10 * @Version 1.0 */ public class ReEnterLockDemo { // ReentrantLock 可重入演示 static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println("=======外层"); lock.lock(); try { System.out.println("=======内层"); } finally { lock.unlock(); //正常情况,加锁几次就要解锁几次 } } finally { lock.unlock(); //正常情况,加锁几次就要解锁几次 } }, "t1").start(); } }
-
程序运行结果:在同一个线程内部成功获取同一把锁
代码示例二:加锁几次就要解锁几次
错误示例:
-
代码
public class ReEnterLockDemo { // ReentrantLock 可重入演示 static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println("=======外层"); lock.lock(); try { System.out.println("=======内层"); } finally { lock.unlock(); } } finally { //实现加锁次数和释放次数不一样 //由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。 //lock.unlock(); //正常情况,加锁几次就要解锁几次 } }, "t1").start(); new Thread(() -> { lock.lock(); try { System.out.println("b thread----外层调用lock"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t2").start(); } }
-
程序运行结果:执行到 t2 线程卡死,这是因为 t1 线程加了两次锁,但是之释放了一次锁,因此 t2 线程拿不到锁,程序无法正常结束
正确示例:
-
代码
public class ReEnterLockDemo { // ReentrantLock 可重入演示 static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println("=======外层"); lock.lock(); try { System.out.println("=======内层"); } finally { lock.unlock(); } } finally { //实现加锁次数和释放次数不一样 //由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。 lock.unlock(); //正常情况,加锁几次就要解锁几次 } }, "t1").start(); new Thread(() -> { lock.lock(); try { System.out.println("b thread----外层调用lock"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t2").start(); } }
-
程序运行结果:t1 线程加了两次锁,释放了两次锁,t2 线程可以拿到锁,等到 t2 线程执行完后,程序结束
3、LockSupport
3.1、LockSupport 是什么?
LockSupport 是个什么鸡毛玩意儿?
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,可以将其看作是线程等待唤醒机制(wait/notify)的加强版
3.2、3种线程等待唤醒的方法
3种让线程等待和唤醒的方法
方式1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
方式2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
3.2.1、Object 类
Object类中的wait和notify方法实现线程等待和唤醒
1、正常情况:实现线程的等待和唤醒
-
代码
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } }, "A").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } }, "B").start(); }
-
程序运行结果:A 线程先执行,执行
objectLock.wait()
后被阻塞,B 线程在 A 线程之后执行objectLock.notify()
将 A线程唤醒
2、异常情况一:不在 synchronized 关键字中使用 wait() 和 notify() 方法
-
代码
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { //synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); //} }, "A").start(); new Thread(() -> { //synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); //} }, "B").start(); }
-
程序运行结果:不在 synchronized 关键字中使用 wait() 和 notify() 方法 ,将抛出
java.lang.IllegalMonitorStateException
异常
3、异常情况二:先 notify() 后 wait()
-
代码
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } }, "A").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } }, "B").start(); }
-
程序运行结果:B 线程先执行
objectLock.notify()
,A 线程再执行objectLock.wait()
,这样 A 线程无法被唤醒
小总结
wait和notify方法必须要在同步块或者方法里面且成对出现使用
先wait后notify才OK
3.2.2、Condition接 口
Condition接口中的await后signal方法实现线程的等待和唤醒
1、正常情况:实现线程的等待和唤醒
-
代码
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { lock.unlock(); } }, "A").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { lock.unlock(); } }, "B").start(); }
-
程序运行结果:A 线程先执行,执行
condition.await()
后被阻塞,B 线程在 A 线程之后执行condition.signal()
将 A线程唤醒
2、异常情况一:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法
-
代码
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { //lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { //lock.unlock(); } }, "A").start(); new Thread(() -> { //lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { //lock.unlock(); } }, "B").start(); }
-
程序运行结果:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法,将抛出
java.lang.IllegalMonitorStateException
异常
3、异常情况二:先 signal() 后 await()
-
代码
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { lock.unlock(); } }, "A").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { lock.unlock(); } }, "B").start(); }
-
程序运行结果:B 线程先执行
condition.signal()
,A 线程再执行condition.await()
,这样 A 线程无法被唤醒
传统的 synchronized 和 Lock 实现等待唤醒通知的约束
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
3.2.3、LockSupport 类
通过 LockSupport 类实现线程的阻塞与唤醒~~~
LockSupport类中的park等待和unpark唤醒,详细见下面讲解
3.3、LockSupport 类
3.3.1、LockSupport 是什么?
官方文档解释
LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和零,默认是零。
可以把许可看成是一种(0, 1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是 1。
3.3.2、LockSupport 的主要方法
1、主要方法
2、阻塞
park()
/park(Object blocker)
park()
方法的作用:阻塞当前线程/阻塞传入的具体线程
permit 默认是 0,所以一开始调用 park()
方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park()
方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
park() 方法通过 Unsafe 类实现
// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
UNSAFE.park(false, 0L);
}
3、唤醒
unpark(Thread thread)
unpark()
方法的作用:唤醒处于阻断状态的指定线程
调用 unpark(thread)
方法后,就会将 thread 线程的许可 permit 设置成 1(注意多次调用 unpark()
方法,不会累加,permit 值还是 1),这会自动唤醒 thread 线程,即之前阻塞中的LockSupport.park()
方法会立即返回。
unpark()
方法通过 Unsafe 类实现
// Makes available the permit for the given thread
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
3.3.3、LockSupport 代码示例
1、正常使用 LockSupport
-
代码
private static void lockSupportParkUnpark() { Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); LockSupport.park(); // 线程 A 阻塞 System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); }, "A"); a.start(); new Thread(() -> { LockSupport.unpark(a); // B 线程唤醒线程 A System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); }, "B").start(); }
-
程序运行结果:A 线程先执行
LockSupport.park()
方法将通行证(permit)设置为 0,其实这并没有什么鸟用,因为 permit 初始值本来就为 0,然后 B 线程执行LockSupport.unpark(a)
方法将 permit 设置为 1,此时 A 线程可以通行
2、先 unpark() 后 park()
-
代码
private static void lockSupportParkUnpark() { Thread a = new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis()); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis()); }, "A"); a.start(); new Thread(() -> { LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); }, "B").start(); }
-
程序运行结果:因为引入了通行证的概念,所以先唤醒(
unpark()
)其实并不会有什么影响,从程序运行结果可以看出,A 线程执行LockSupport.park()
时并没有被阻塞
3、异常情况:没有考虑到 permit 上限值为 1
-
代码
private static void lockSupportParkUnpark() { Thread a = new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis()); LockSupport.park(); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis()); }, "A"); a.start(); new Thread(() -> { LockSupport.unpark(a); LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); }, "B").start(); }
-
程序运行结果:由于 permit 的上限值为 1,所以执行两次
LockSupport.park()
操作将导致 A 线程阻塞
LockSupport 小总结
以前的两种方式:
以前的等待唤醒通知机制必须synchronized里面执行wait和notify,在lock里面执行await和signal,这上面这两个都必须要持有锁才能干
LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点
LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死
3.3.4、LockSupport 重点说明
1、LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
2、LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
3、形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
- 当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
- 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
3.4、LockSupport 面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
4、AbstractQueuedSynchronizer
AbstractQueuedSynchronizer之AQS:抽象的队列同步器
4.1、AQS 前置知识
前置知识
- 公平锁和非公平锁
- 可重入锁
- LockSupport
- 自旋锁
- 数据结构之链表
- 设计模式之模板设计模式
4.2、AQS 是什么?
1、字面意思
AQS(AbstractQueuedSynchronizer):抽象的队列同步器
一般我们说的 AQS 指的是 java.util.concurrent.locks
包下的 AbstractQueuedSynchronizer,但其实还有另外三种抽象队列同步器:AbstractOwnableSynchronizer
、AbstractQueuedLongSynchronizer
和 AbstractQueuedSynchronizer
2、技术翻译
AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态
CLH:Craig、Landin and Hagersten 队列,是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
4.3、AQS 是 JUC 的基石
和AQS有关的并发编程类
举几个常见的例子
-
ReentrantLock
-
CountDownLatch
-
ReentrantReadWriteLock
-
Semaphore
-
…
进一步理解锁和同步器的关系
锁,面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。
同步器,面向锁的实现者。比如Java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。
4.4、AQS 能干嘛
AQS:加锁会导致阻塞
有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定 会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
4.5、AQS 初步认识
1、AQS初识
官网解释
有阻塞就需要排队,实现排队必然需要队列
- AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
- Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用
static class Node<K,V> implements Map.Entry<K,V> {
来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread - 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客
2、AQS内部体系架构
0、AQS 内部体系框架
1、AQS的int变量
AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去
/**
* The synchronization state.
*/
private volatile int state;
2、AQS的CLH队列
CLH队列(三个大牛的名字组成),为一个双向队列,类似于银行侯客区的等待顾客
3、内部类Node(Node类在AQS类内部)
Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
Node类的内部结构
static final class Node{
//共享
static final Node SHARED = new Node();
//独占
static final Node EXCLUSIVE = null;
//线程被取消了
static final int CANCELLED = 1;
//后继线程需要唤醒
static final int SIGNAL = -1;
//等待condition唤醒
static final int CONDITION = -2;
//共享式同步状态获取将会无条件地传播下去
static final int PROPAGATE = -3;
// 初始为e,状态是上面的几种
volatile int waitStatus;
// 前置节点
volatile Node prev;
// 后继节点
volatile Node next;
// ...
4、总结
有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现
3、AQS同步队列的基本结构
4、AQS底层是怎么排队的?
通过调用 LockSupport.pork()
来进行排队
4.6、从 ReentrantLock 进入 SQS
4.6.1、ReentrantLock 锁
ReentrantLock 锁是个啥玩意儿?
ReentrantLock
类是 Lock
接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReentrantLock 的原理
ReentrantLock
实现了 Lock 接口,在 ReentrantLock
内部聚合了一个 AbstractQueuedSynchronizer
的实现类
4.6.2、公平锁 & 非公平锁
通过
ReentrantLock
的源码来讲解公平锁和非公平锁
在 ReentrantLock
内定义了静态内部类,分别为 NoFairSync
(非公平锁)和 FairSync
(公平锁)
ReentrantLock
的构造函数:不传参数表示创建非公平锁;参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁
捞一眼 lock()
方法的执行流程:以 NonfairSync
为例
在 ReentrantLock
中,NoFairSync
和 FairSync
中 tryAcquire()
方法的区别,可以明显看出公平锁与非公平锁的lock()
方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors()
方法是公平锁加锁时判断等待队列中是否存在有效节点的方法
公平锁与非公平锁的总结
对比公平锁和非公平锁的tryAcqure()
方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors()
,hasQueuedPredecessors()
中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
-
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
-
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一 个排队线程在
unpark()
,之后还是需要竞争锁(存在线程竞争的情况下)
而 acquire()
方法最终都会调用 tryAcquire()
方法
在 NonfairSync
和 FairSync
中均重写了其父类 AbstractQueuedSynchronizer
中的 tryAcquire()
方法
4.6.3、从非公平锁的 lock() 入手
先从示例代码入手
源码解读比较困难,我们这里举个栗子,假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个个,我们使用 lock.lock()
模拟这种情况
/**
* @ClassName AQSDemo
* @Description TODO
* @Author Oneby
* @Date 2021/1/21 11:08
* @Version 1.0
*/
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
// 3个线程模拟3个来银行网点,受理窗口办理业务的顾客
// A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try {
System.out.println("-----A thread come in");
try {
TimeUnit.MINUTES.sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "A").start();
// 第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("-----B thread come in");
} finally {
lock.unlock();
}
}, "B").start();
// 第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("-----C thread come in");
} finally {
lock.unlock();
}
}, "C").start();
}
}
先来看看线程 A(客户 A)的执行流程
之前已经讲到过,new ReentrantLock()
不传参默认是非公平锁,调用 lock.lock()
方法最终都会执行 NonfairSync
重写后的 lock()
方法
第一次执行 lock() 方法
由于第一次执行 lock()
方法,state
变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1)
CAS 判断,可得 state == expected == 0
,因此 CAS 成功,将 state
的值修改为 1
再来复习下 CAS:通过 Unsafe
提供的 compareAndSwapXxx()
方法保证修改操作的原子性(通过 CPU 原语保证),如果变量的值等于期望值,则修改变量的值为 update
,并返回 true
;若不等,则返回 false
。this
代表当前对象,stateOffset
表示 state
变量在该对象中的偏移量
再来看看 setExclusiveOwnerThread()
方法做了啥:将拥有 lock 锁的线程修改为线程 A
再来看看线程 B(客户 B)的执行流程
第二次执行 lock() 方法
由于第二次执行 lock()
方法,state
变量的值等于 1,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1)
CAS 判断,可得 state != expected
,因此 CAS 失败,进入 acquire()
方法
acquire()
方法主要包含如下几个方法,下面我们一个一个来讲解
tryAcquire(arg)
方法的执行流程
先来看看 tryAcquire()
方法,诶,怎么抛了个异常?别着急,仔细一看是 AbstractQueuedSynchronizer
抽象队列同步器中定义的方法,既然抛出了异常,就证明父类强制要求子类去实现
Ctrl + Alt + B 找到子类中的实现
这里以非公平锁 NonfairSync
为例,在 tryAcquire()
方法中调用了 nonfairTryAcquire()
方法,注意,这里传入的参数都是 1
nonfairTryAcquire(acquires)
正常的执行流程:
在 nonfairTryAcquire()
方法中,大多数情况都是如下的执行流程:线程 B 执行 int c = getState()
时,获取到 state
变量的值为 1,表示 lock 锁正在被占用;于是执行 if (c == 0) {
发现条件不成立,接着执行下一个判断条件 else if (current == getExclusiveOwnerThread()) {
,current
线程为线程 B,而 getExclusiveOwnerThread()
方法返回正在占用 lock 锁的线程,为线程 A,因此 tryAcquire()
方法最后会 return false
,表示并没有抢占到 lock 锁
补充:getExclusiveOwnerThread()
方法返回正在占用 lock 锁的线程(排他锁,exclusive)
nonfairTryAcquire(acquires)
比较特殊的执行流程:
第一种情况是,走到 int c = getState()
语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state
变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true
,表示抢占锁成功。其实这里还有一种情况,需要留到 unlock()
方法才能说清楚
第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然示例代码里面并没有体现出来),这时 current == getExclusiveOwnerThread()
条件成立,将 state
变量的值加上 acquire
,这种情况下也应该 return true
,表示线程 A 正在占用 lock 锁。因此,state
变量的值是可以大于 1 的
继续往下走,执行
ddWaiter(Node.EXCLUSIVE)
方法
在 tryAcquire()
方法返回 false
之后,进行 !
操作后为 true
,那么会继续执行 addWaiter()
方法
来看看 addWaiter()
方法做了些啥?
之前讲过,Node
节点用于封装用户线程,这里将当前正在执行的线程通过 Node
封装起来(当前线程正是抢占 lock 锁没有抢占到的线程)
判断 tail 尾指针是否为空,双端队列此时还没有元素呢~肯定为空呀,那么执行 enq(node)
方法,将封装了线程 B 的 Node
节点入队
enq(node) 方法:构建双端同步队列
也许看到这里的代码有点蒙,需要有些前置知识,在双端同步队列中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。
第一次执行 for 循环:现在解释起来就不费劲了,当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。此时 tail == null
,因此进入 if(t == null) {
的分支,头指针指向哨兵节点,此时队列中只有一个节点,尾节点即是头结点,因此尾指针也指向该哨兵节点
第二次执行 for 循环:现在该将装着线程 B 的节点放入双端同步队列中,此时 tail
指向了哨兵节点,并不等于 null
,因此 if (t == null)
不成立,进入 else
分支。以尾插法的方式,先将 node
(装着线程 B 的节点)的 prev
指向之前的 tail
,再将 node 设置为尾节点(执行 compareAndSetTail(t, node)
),最后将 t.next
指向 node
,最后执行 return t
结束 for 循环
补充:compareAndSetTail(t, node)
方法的实现
注意:哨兵节点和 nodeB
节点的 waitStatus
均为 0,表示在等待队列中
acquireQueued()
方法的执行
执行完 addWaiter()
方法之后,就该执行 acquireQueued()
方法了,这个方法有点东西,我们放到后面再去讲它
最后来看看线程 C(客户 C)的执行流程
线程 C 和线程 B 的执行流程很类似,都是执行 acquire()
中的方法
但是在 addWaiter()
方法中,执行流程有些区别。此时 tail != null
,因此在 addWaiter()
方法中就已经将 nodeC
添加至队尾了
执行完 addWaiter()
方法后,就已经将 nodeC 挂在了双端同步队列的队尾,不需要再执行 enq(node)
方法
补前面的坑:
acquireQueued()
方法的执行逻辑
先来看看看看 acquireQueued()
方法的源代码,其实这样直接看代码有点懵逼,我们接下来举例来理解。注意看:两个 if
判断中的代码都放在 for( ; ; )
中执行,这样可以实现自旋的操作
线程 B 的执行流程
线程 B 执行 addWaiter()
方法之后,就进入了 acquireQueued()
方法中,此时传入的参数为封装了线程 B 的 nodeB
节点,nodeB
的前驱结点为哨兵节点,因此 final Node p = node.predecessor()
执行完后,p
将指向哨兵节点。哨兵节点满足 p == head
,但是线程 B 执行 tryAcquire(arg)
方法尝试抢占 lock 锁时还是会失败,因此会执行下面 if
判断中的 shouldParkAfterFailedAcquire(p, node)
方法,该方法的代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
哨兵节点的 waitStatus == 0
,因此执行 CAS 操作将哨兵节点的 waitStatus
改为 Node.SIGNAL(-1)
注意:compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
调用 unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
实现,虽然 compareAndSwapInt()
方法内无自旋,但是在 acquireQueued()
方法中的 for( ; ; )
能保证此自选操作成功(另一种情况就是线程 B 抢占到 lock 锁)
执行完上述操作,将哨兵节点的 waitStatus
设置为了 -1
执行完毕将退出 if
判断,又会重新进入 for( ; ; )
循环,此时执行 shouldParkAfterFailedAcquire(p, node)
方法时会返回 true
,因此此时会接着执行 parkAndCheckInterrupt()
方法
线程 B 调用 park()
方法后被挂起,程序不会然续向下执行,程序就在这儿排队等待
线程 C 的执行流程
线程 C 最终也会执行到 LockSupport.park(this);
处,然后被挂起,进入等待区
总结:
如果前驱节点的 waitstatus
是 SIGNAL
状态(-1),即 shouldParkAfterFailedAcquire()
方法会返回 true
,程序会继续向下执行 parkAndCheckInterrupt()
方法,用于将当前线程挂起
根据 park()
方法 API 描述,程序在下面三种情况会继续向下执行:
- 被 unpark
- 被中断(interrupt)
- 其他不合逻辑的返回才会然续向下执行
因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态。如果程序由于被中断,该方法会返回 true
4.6.4、可总算要 unlock() 了
线程 A 执行
unlock()
方法
A 线程终于要 unlock()
了吗?真不容易啊!
unlock()
方法调用了 sync.release(1)
方法
release()
方法的执行流程
其实主要就是看看 tryRelease(arg)
方法和 unparkSuccessor(h)
方法的执行流程,这里先大概说以下,能有个印象:线程 A 即将让出 lock 锁,因此 tryRelease()
执行后将返回 true
,表示礼让成功,head
指针指向哨兵节点,并且 if
条件满足,可执行 unparkSuccessor(h)
方法
tryRelease(arg)
方法的执行逻辑
又是 AbstractQueuedSynchronizer
类中定义的方法,又是抛了个异常
老样子 Ctrl + Alt + B,查看其具体实现
线程 A 只加锁过一次,因此 state
的值为 1,参数 release
的值也为 1,因此 c == 0
。将 free
设置为 true
,表示当前 lock 锁已被释放,将排他锁占有的线程设置为 null
,表示没有任何线程占用 lock 锁
unparkSuccessor(h)
方法的执行逻辑
在 release()
方法中获取到的头结点 h
为哨兵节点,h.waitStatus == -1
,因此执行 CAS操作将哨兵节点的 waitStatus
设置为 0,并将哨兵节点的下一个节点(s = node.next = nodeB
)获取出来,并唤醒 nodeB
中封装的线程(if (s == null || s.waitStatus > 0)
不成立,只有 if (s != null)
成立)
执行完上述操作后,当前占用 lock 锁的线程为 null
,哨兵节点的 waitStatus
设置为 0,state
的值为 0(表示当前没有任何线程占用 lock 锁)
杀个回马枪:继续来看 B 线程被唤醒之后的执行逻辑
再次回到 lock()
方法的执行流程中来,线程 B 被 unpark()
之后将不再阻塞,继续执行下面的程序,线程 B 正常被唤醒,因此 Thread.interrupted()
的值为 false
,表示线程 B 未被中断
回到上一层方法中,此时 lock 锁未被占用,线程 B 执行 tryAcquire(arg)
方法能够抢到 lock 锁,并且将 state
变量的值设置为 1,表示该 lock 锁已经被占用
接着来研究下 setHead(node)
方法:传入的节点为 nodeB
,头指针指向 nodeB
节点;将 nodeB
中封装的线程置为 null
(因为已经获得锁了);nodeB
不再指向其前驱节点(哨兵节点)。这一切都是为了将 nodeB
作为新的哨兵节点
执行完 setHead(node)
方法的状态如下图所示
将 p.next
设置为 null
,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB
作为新的哨兵节点
哇哦,通透,线程 C 也是类似的执行流程
4.7、AQS 总结
AQS 的考点
第一个考点:我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
答:3个状态:没占用是0,占用了是1,大于1是可重入锁
第二个考点:如果锁正在被占用,AB两个线程进来了以后,请问这个总共有多少个Node节点?
答:答案是3个,分别是哨兵节点、nodeA、nodeB
AQS 源码解读案例图示