AQS(AbstractQueueSynchronizer)详解


概述

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS 核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

原理

AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。

AQS维护一个共享变量state,来表示同步状态

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过procted类型的getState,setState,compareAndSetState进行操作。可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)
AQS 定义两种资源共享方式:

  • Exclusive(独占,只有一个线程能执行,如ReentrantLock)
  • Share(共享,多个线程可同时执行, 如Semaphore/CountDownLatch)
  1. 独占式

在这里插入图片描述

  1. 共享式

AQS通过CLH"队列来完成获取资源线程的排队工作

(Craig、Landin and Hagersten 队列,是单向链表,AQS使用CLH的变体,为双向链表)。
该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。线程被封装成Node进入CLH队列并阻塞。
在这里插入图片描述
在这里插入图片描述

自定义同步器

自定义同步器实现的相关方法也只是为了通过修改 State 字段来实现多线程的独占模式或者共享模式
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:

钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。

//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()

不可重写的方法

/*Acquires in exclusive mode, ignoring interrupts.*/
//独占式获取同步状态,如果当前线程获取同步状态成功,立即返回。否则,将会进入同步队列等待,
//该方法将会重复调用重写的tryAcquire(int arg)方法
public final void acquire(int arg)
 
/*Acquires in exclusive mode, aborting if interrupted.*/
//与acquire(int arg)基本相同,但是该方法响应中断。
public final void acquireInterruptibly(int arg)
 
/* Releases in exclusive mode.  Implemented by unblocking one or more threads if {@link #tryRelease} returns true.
This method can be used to implement method {@link Lock#unlock}.*/
//独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒
public final boolean release(int arg)

通过调用同步器的acquire(int arg)方法可以获取同步状态

    public final void acquire(int arg) {//**该方法是模板方法**
        if (!tryAcquire(arg) &&//先通过tryAcquire获取同步状态
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//获取同步状态失败则生成节点并加入同步队列
            selfInterrupt();
    }
  1. 首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入2步骤
  2. 此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部,此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加:compareAndSetTail(Node expect,Node update)它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
  //将节点加入到同步队列的尾部
 private Node addWaiter(Node mode) {
       Node node = new Node(Thread.currentThread(), mode);//生成节点(Node)
       // Try the fast path of enq; backup to full enq on failure
       //快速尝试在尾部添加
       Node pred = tail;
       if (pred != null) {
           node.prev = pred;//先将当前节点node的前驱指向当前tail
           if (compareAndSetTail(pred, node)) {//CAS尝试将tail设置为node
               //如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail成功
               //只需要将"之前的tail"的后继节点指向node即可
               pred.next = node;
               return node;
           }
       }
       enq(node);//否则,通过死循环来保证节点的正确添加
       return node;
   }
private Node enq(final Node node) {
       for (;;) {//通过死循环来保证节点的正确添加
           Node t = tail;
           if (t == null) { // Must initialize 同步队列为空的情况
               if (compareAndSetHead(new Node()))
                   tail = head;
           } else {
               node.prev = t;
               if (compareAndSetTail(t, node)) {//直到CAS成功为止
                   t.next = node;
                   return t;//结束循环
               }
           }
       }
   }
  1. 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态,若获取不到,则阻塞结点线程LockSupport.park(...),直到被前驱结点唤醒或者被中断。
    final boolean acquireQueued(final Node node, int arg) {
       boolean failed = true;
       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())//阻塞当前线程
                   interrupted = true;
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }
    /**Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal control in all acquire loops.*/
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//获取前驱节点的等待状态
        if (ws == Node.SIGNAL)
        //SIGNAL状态:前驱节点释放同步状态或者被取消,将会通知后继节点。因此,可以放心的阻塞当前线程,返回true。
            /* 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 {//独占模式下,一般情况下这里指前驱节点等待状态为SIGNAL
            /* 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);//设置当前节点等待状态为SIGNAL
        }
        return false;
    }

    /** Convenience method to park and then check if interrupted 。return {@code true} if interrupted */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//阻塞当前线程
        return Thread.interrupted();
    }

总的流程图,这也是ReentrantLock.lock()的流程:
在这里插入图片描述

通过调用同步器的release(int arg)方法可以释放同步状态

    public final boolean release(int arg) {
        if (tryRelease(arg)) {//释放同步状态
            Node h = head;
            if (h != null && h.waitStatus != 0)//独占模式下这里表示SIGNAL
                unparkSuccessor(h);//唤醒后继节点
            return true;
        }
        return false;
    }
    /** Wakes up node's successor, if one exists.*/
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;//获取当前节点等待状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);//更新等待状态
 
        /* Thread to unpark is held in successor, which is normally just the next node.  
           But if cancelled or apparently null,
         * traverse backwards from tail to find the actual non-cancelled successor.*/
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {//找到第一个没有被取消的后继节点(等待状态为SIGNAL)
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//唤醒后继线程
    }

该方法在释放了同步状态之后,会"唤醒"其后继节点(进而使后继节点重新尝试获取同步状态)LockSupport.unpark(...)

具体应用

在这里插入图片描述

ReentrantLock

在这里插入图片描述
这篇博客写得实在是太详细了: https://blog.csdn.net/wwj17647590781/article/details/117574639?spm=1001.2014.3001.5501

Semaphore(信号量)

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。

Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。

// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();

以无参 acquire 方法为例,调用semaphore.acquire() ,线程尝试获取许可证,

  • 如果 state > 0 的话,则表示可以获取成功。尝试使用 CAS 操作去修改 state 的值 state=state-1
  • 如果 state <= 0 的话,则表示许可证数量不足,会创建一个 Node 节点加入等待队列,挂起当前线程

以无参 release 方法为例,调用semaphore.release() ,线程尝试释放许可证,并使用 CAS 操作去修改 state 的值 state=state+1。释放许可证成功之后,同时会唤醒等待队列中的一个线程。被唤醒的线程会重新尝试去修改 state 的值 state=state-1 ,如果 state > 0 则获取令牌成功,否则重新进入等待队列,挂起线程。

CountDownLatch

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

CountDownLatch 的两种典型用法:

  1. 某一线程在开始运行前等待 n 个线程执行完毕 : 将 CountDownLatch 的计数器初始化为 n (new CountDownLatch(n)),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()),当计数器的值变为 0 时,在 CountDownLatch 上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。主线程.await()
  2. 实现多个线程开始执行任务的最大并行性:注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。非主线程.await()
public class CountDownLatchExample {
  // 请求的数量
  private static final int THREAD_COUNT = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    // 只是测试使用,实际场景请手动赋值线程池参数
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
    for (int i = 0; i < THREAD_COUNT; i++) {
      final int threadNum = i;
      threadPool.execute(() -> {
        try {
          test(threadNum);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          // 表示一个请求已经被完成
          countDownLatch.countDown();
        }

      });
    }
    countDownLatch.await();
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);
    System.out.println("threadNum:" + threadnum);
    Thread.sleep(1000);
  }
}

CyclicBarrier(循环栅栏)

它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CountDownLatch 的实现是基于AQS的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。


parties为每次拦截的线程数
count作为计数器,每当一个线程到了栅栏这里,就将计数器-1,当count==0时,表示这一代的最后一个线程到达栅栏,执行构造器中输入的任务。

//每次拦截的线程数
private final int parties;
//计数器
private int count;

当调用 CyclicBarrier 对象的 await() 方法时,实际上调用的是 dowait(false, 0L)方法。 await() 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。然后开启下一波栅栏。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值