Java 队列同步器 AQS(AbstractQueuedSynchronizer)源码解析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_45505313/article/details/100923717

1. AQS 同步原理

1.1 核心思想

如我们所知,同步是为了解决共享资源的使用安全问题,其实现核心主要分为两个部分:

  1. 锁的语义实现
    就是使用某种共享资源作为锁,线程通过争用这个资源从而获得执行权
  2. 线程调度
    表现为锁争用失败后对线程的处理策略,以及锁释放后线程的唤醒和锁分配

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那就将暂时获取不到资源的线程封装成一个 Node 节点加入到队列中阻塞等待。当持有资源的线程释放资源时,会唤醒后继结点,唤醒的结点线程继续加入到对资源的争夺中

  • AQS 锁的语义实现
    使用一个 int 成员变量来表示同步状态,通过 CAS 对该同步状态进行原子操作实现对值的修改

     /**
      * The synchronization state.
      */
     private volatile int state;//共享变量,使用volatile修饰保证线程可见性
    
  • AQS 线程调度
    通过 FIFO 队列来完成获取资源线程的排队工作,这个机制在 AQS 中是用 CLH 队列(虚拟双向队列,不存在队列实例仅存在结点间的指向关系) 实现的,即将暂时获取不到资源的线程封装成一个 Node 节点加入到队列中

    To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you 
    just set the head field.
    
      *      +------+  prev +-----+  prev +-----+
      * head | Node | <---- | Node| <---- | Node|  tail
      *      |      | ----> |     | ----> |     |
      *      +------+ next  +-----+ next  +-----+
    
    static final class Node {
     	int waitStatus;
     	Node prev;
     	Node next;
     	Node nextWaiter;
     	Thread thread;
    }
    

1.2 具体实现

1.2.1 获取锁过程

AQS 使用 acquire() 方法来尝试获取锁,对中断不敏感,完成synchronized语义

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

上述逻辑主要包括:

  1. 尝试获取锁(调用tryAcquire()尝试更改状态,需要保证原子性),在子类重写的tryAcquire方法中使用同步器提供的对state操作的方法,利用CAS保证只有一个线程能够对状态成功修改,而没有成功修改的线程将进入sync队列排队
  2. 如果获取不到锁,调用 addWaiter()将当前线程构造成节点Node并加入等待队列。每个新的节点Node 入队都会将其放在尾部,并更新 tail 尾部指针指向它,从而形成了一个虚拟双向队列,这样做的目的是线程间的通信会被限制在较小规模(也就是两个节点左右)
  3. acquireQueued()结点在队列中空循环一直尝试获取锁,直到当前节点的 prev 节点成为 head 节点并且 tryAcquire()资源成功,则设置当前节点为 head 并退出等待队列;在空循环中如检测到需要阻塞当前线程,则调用 parkAndCheckInterrupt()将当前线程从线程调度器上摘下,进入等待状态

下面的流程图基本描述了一次acquire所需要经历的过程:

在这里插入图片描述

1.2.2 释放锁过程

AQS 使用 release()方法将状态设置回去,也就是将锁释放

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

上述逻辑主要包括:

  1. 尝试释放资源,tryRelease() 通过 CAS保证原子化的将状态设置回去,如果释放状态成功过则将进入后继节点的唤醒过程
  2. unparkSuccessor()尝试唤醒当前节点的后继节点所包含的线程,如果满足状态(没有被cancel掉)就进行唤醒操作,如果不满足状态,则从尾部开始找寻符合要求的节点,之后通过LockSupport 的unpark()方法唤醒节点,使其继续 acquire()资源

1.3 AQS 的资源共享方式

  1. 独占式 (Exclusive)
    对于独占式同步组件,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire()返回值为boolean

  2. 共享式 (Share)
    对于共享式同步组件,同一时刻可以有多个线程同时获取到同步状态,其待重写的尝试获取同步状态的方法tryAcquireShared()返回值为int,也就是剩余的共享资源数量

    1. 当返回值 > 0时,表示还有剩余同步状态可供其他线程获取,获取同步状态成功
    2. 当返回值 = 0时,表示获取同步状态成功,但没有可用同步状态了
    3. 当返回值 < 0时,表示获取同步状态失败

2. JDK 中 AQS 的应用

2.1 ReentrantLock

ReentrantLock 的 state初始化为 0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock() 到state=0(即释放锁)为止,其它线程才有机会获取该锁。释放锁之前,A线程自己可以重复获取此锁(state累加),这是可重入的概念。但获取多少次就要释放多么次,这样才能保证state回到 0 值

2.2 Semaphore

Semaphore(计数信号量)-允许多个线程同时访问,经常用于限制获取某种资源的线程数量

2.3 CountDownLatch

CountDownLatch(倒计时器)任务分为N个子线程去执行,state 也初始化为N(N要与线程个数一致)。这N个子线程并行执行,每个子线程执行完后countDown()一次,state就 CAS 减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续运行

  • CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用

2.4 CyclicBarrier

CyclicBarrier(循环屏障) 和 CountDownLatch 非常类似,它也可以实现线程间的互相等待,但是它的功能比 CountDownLatch 更加复杂和强大。

CyclicBarrier 要做的事情是,让一组线程到达一个屏障(也可以叫同步点)之前被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才能继续运行。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 自身已经到达了屏障,然后进入阻塞状态。

  • 应用场景
    CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/100923717