AbstractQueuedSynchronizer 源码分析、学习笔记、设计思想学习;仅供参考

并发前言

在程序中如果有多个线程执行对同一资源进行操作,我们讲发生了并发行为。而我们常说的并发操作指的是写,因为并发读往往考虑的是服务器资源,而写操作会影响到程序是否正常运行。处理并发写,处理的方法无非这几种思想:

  1. 排队,让多个线程排队,有序进行,这个过程也叫同步
  2. 互斥,很多的线程,但只能有一个线程在执行,这种情况一般加锁来保证

    基于 ReentrantLock 的 demo

几个 demo

描述:让两个线程完成对一个数累加

demo1:先看一个有问题的 demo:

  1. private static int num = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. MyThread t2 = new MyThread();
  5. t1.start();
  6. t2.start();
  7. t1.join();
  8. t2.join();
  9. // 这里num输出结果小于 2 * 1000000
  10. System.out.println("num: " + num);
  11. }
  12. static class MyThread extends Thread {
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 1000000; i++) {
  16. num++;
  17. }
  18. }
  19. }

demo2:修改后的 demo:

  1. private static int num = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. MyThread t1 = new MyThread();
  4. MyThread t2 = new MyThread();
  5. t1.start();
  6. t1.join();
  7. t2.start();
  8. t2.join();
  9. // 输出正确结果 2 * 1000000
  10. System.out.println("num: " + num);
  11. }
  12. static class MyThread extends Thread {
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 1000000; i++) {
  16. num++;
  17. }
  18. }
  19. }

demo3:使用 ReenTrantLock 的 demo:

  1. private static int num = 0;
  2. private static ReentrantLock lock = new ReentrantLock();
  3. public static void main(String[] args) throws InterruptedException {
  4. MyThread t1 = new MyThread();
  5. MyThread t2 = new MyThread();
  6. t1.start();
  7. t2.start();
  8. t1.join();
  9. t2.join();
  10. // 输出正确结果 2 * 1000000
  11. System.out.println("num: " + num);
  12. }
  13. static class MyThread extends Thread {
  14. @Override
  15. public void run() {
  16. try {
  17. // 加锁
  18. lock.lock();
  19. for (int i = 0; i < 1000000; i++) {
  20. num++;
  21. }
  22. } finally {
  23. // 释放锁
  24. lock.unlock();
  25. }
  26. }
  27. }

demo 分析

num++ 这个操作,在底层指令操作其实是分成了好几步执行的。

demo1 有问题是因为 t1 对值修改了,但是 t2 对该值进行了重复操作。
demo2 能达到正确的效果,正是我们一开始提到的排队思想,我们使用了 join() 方法,手动让线程排队执行了。
demo3 也能实现正确的效果,是因为我们用了互斥思想,进行加锁,同一时刻只有一个线程在执行。

AQS 设计思想

AbstractQueuedSynchronizer 简称 AQS 抽象队列同步器,从名称看出来它是一个抽象类、具有队列功能、是可以同步的。JUC 下大多数并发工具包是基于它来实现的,它定义了基本的操作接口,可以让使用者自定义实现功能。上面的 ReenTrantLock 正是基于 AQS 实现的。

AQS 的源码点进去看有很多,有点复杂,好在注释比代码多。先抓它的设计思想、核心要点,再看细节。

  1. 有一个 FIFO 的队列;
  2. 有一个 int state 变量来表示当前锁的状态;
  3. 有一个 CHL 队列(CHL 三个人名首字母缩写),该队列非常重要,后面展开
  4. AQS 类中的 Node 里的属性大多用 volatile 修饰,保证了可见性
  5. 获取锁,获取不到就放到队列

大致先有个印象,后面 debug 详解。

ReentrantLock 源码 Debug

AQS 源码要点

先看 AQS 几个重要点:

  1. Node 节点,存在队列中,它有以下属性:

    1. // 共享模式
    2. static final Node SHARED = new Node();
    3. // 独占模式
    4. static final Node EXCLUSIVE = null;
    5. /** waitStatus 值的状态 表示该线程已取消 */
    6. static final int CANCELLED = 1;
    7. /** waitStatus 值的状态 表示该线程需要释放锁(唤醒) */
    8. static final int SIGNAL = -1;
    9. /** waitStatus 值的状态 表示该线程需要条件等待 */
    10. static final int CONDITION = -2;
    11. // 不知道干嘛的
    12. static final int PROPAGATE = -3;
    13. volatile int waitStatus;
    14. // 队列的前继节点
    15. volatile Node prev;
    16. // 队列的前继节点
    17. volatile Node next;
    18. // 当前线程
    19. volatile Thread thread;
    20. Node nextWaiter;
  2. state 属性 ```java // 0表示空闲,1表示被使用了,大于1表示重入了,重入一次加1 private volatile int state;

protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } ```

ReentrantLock#lock();获取锁操作

首先会用 CAS 操作,对 state 进行赋值,赋值成功说明当前是空闲的,将自己设置为持有锁的线程。
image.png
如果没有赋值成功,会尝试获取锁
image.png
再次获取下,万一有人释放了呢? 如果不等于 0 ,判断当前执行的线程是不是自己,加 1 操作,ReenTrantLock 可重入的功能。

image.png

如果 tryAcquire() 返回了 false,说明没有获取到锁,那么执行 addWaiter(Node.EXCLUSIVE) 加入到队列中
image.png
如果队列已经有节点了,那么将自己放到队尾,只是个队列的入队操作
image.png
入队成功后,死循环去获取,不断重试,也就是自旋操作。
image.png
这里是超级重点!!!
image.png
挂起当前线程
image.png

参考资料