AQS 队列同步器

一、队列同步器AQS

​ AQS是一个为满足同步需求、保障线程安全,构建锁或者其他同步组件的框架或者叫模板、底层实现。我们用它可以实现我们自己像要的同步组件(就像ReentrantLock)。AQS是一个抽象类,使用者需要自定义一个同步器并且继承它,重写它指定的方法(独占式/共享式获取/释放同步状态方法),再将自定义同步器组合在自己定义的同步组件中,当真正用的时候同步组件调自定义同步器的模板方法,模板方法再调我们重写的方法,从设计模式角度来看这是一个很典型的【模板模式】
在这里插入图片描述

它与锁的区别:
锁是应用层,是给我们开发使用的,封装好了线程同步的逻辑,定义了使用者与锁的接口交互;
AQS是实现层,是锁的实现者,是锁的内部实现。

二、框架

在这里插入图片描述

​ AQS内部维护了一个int类型的volatile变量state(同步状态)和一个先进先出的虚拟双向队列,这两个很重要,可以说是实现同步功能的核心。

​ AQS对共享资源定义了两种访问形式:独占式(Exclusive,如:ReentrantLock)和共享式(Share,如:CountDownLatch/Semaphore)

​ 自定义同步器需要重写的方法:

boolean tryAcquire(int)独占式,尝试获取资源,非阻塞立即返回结果
boolan tryRelease(int)独占式,尝试释放资源,非阻塞立即返回结果
int tryAcquireShared(int)共享式,尝试获取资源,非阻塞,返回结果
<0获取失败;>0获取成功,还有多余资源;=0获取成功,资源正好,没有多余资源
boolan tryReleaseShared(int)共享式,尝试释放资源,非阻塞,返回结果 true表示唤醒后继等待节点,否则false
isHeldExclusively()该线程是否正在独占资源

​ AQS提供的模板方法(主要功能:调用以上重写的方法,把获取资源失败的线程加入等待队列)

final void acquire(int arg)独占式获取同步状态,获取失败进入同步等待队列
final void acquireInterruptibly(int arg)独占式,能响应中断,获取进入等待队列,如被中断,抛出中断异常并返回
final boolean tryAcquireNanos(int,long)独占式,支持超时,限定时间内获取则返回true,否则返回false
final boolean release(int arg)独占式,释放同步状态,返回释放是否成功
final void acquireShared(int arg)共享式获取同步状态,失败进入等待队列。同一时间可以支持多线程获取
final void acquireSharedInterruptibly(int )共享式获取,能响应中断
final boolean tryAcquireSharedNanos共享式获取,支持超时
final boolean releaseShared(int arg)共享式,释放同步状态

​ AQS里有两个内部类:Node和Condition。Node很重要,是等待队列的基础,整个同步过程就是围绕Node和state展开的。

Node:构成等待队列的节点,获取资源失败的线程会被封装成一个node,然后加入到等待队列。node包含了线程本身和等待状态waitState,有5中等待状态。

Condition:主要用在同步组件中实现线程通讯,类似于synchronized锁代码块的时候有等待/通知机制(wait/notify),lock也有啊,lock的等待通知机制实现就是condition,并且他比wait/notify更强大,它可以创建多个,实现多个线程之间交叉相互通信。

Node的5中等待状态(理解这5个状态对后边的源码分析很有帮助):

CANCELLED(1)节点已被取消,或者是超时或者是被中断,反正是不再需要获取资源
SIGNAL(-1)节点是这个状态代表它释放资源后要唤醒后继节点,新节点加入队列时,会把新节点的前继节点状态改为Signal,自己就去挂起休息了,等着被通知。这个状态很重要,理解了对后面很有帮助
CONDITION(-2)结点等待在Condition上,当调用了Condition的signal()方法后,节点就从等待队列进入到阻塞队列,等待获取同步资源
PROPAGATE(-3)共享模式下,前继结点会唤醒后继结点,并一直往后唤醒,传播下去
0无含义,默认状态,刚创建的新节点状态

三、应用

​ 模仿ReenactmentLock写一个自己的互斥锁,主要是看怎么实现怎么使用的

package com.bbwt.thread.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


/***
 * 自定义一个同步组件(互斥锁)
 * 假设只提供一个同步资源:state是1
 * 例子式独占式的
 */
public class Mutex implements Lock {

    //自己定义一个同步器,这是一个静态内部类,继承自AQS,需要重写独占式的方法
    private static class Sync extends AbstractQueuedSynchronizer {

        //重写方法:当前同步状态是否被占用
        @Override
      protected  boolean isHeldExclusively(){
            //如果state是1就是被当前线程独占,其他线程拿不到了需要等待
            return getState()==1;
        }
        //重写方法:尝试获取同步资源,独占式
        @Override
        public boolean tryAcquire(int acquires){
            //采用cas设置state状态
            boolean b = compareAndSetState(0, acquires);
            if(b){
                //如果设置成功,代表获取了共享资源,当前线程占有同步状态
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        //重写方法:尝试释放同步状态,独占式
        @Override
        protected boolean tryRelease(int release){
          if(getState()==0) {
              throw  new IllegalMonitorStateException();
          }
          setExclusiveOwnerThread(null);
          setState(0);
          return true;
        }

        Condition newCondition (){
          return new ConditionObject();
        }
    }

    //自己不干,代理给sync,因为sync是自定义得同步器,实现了自己想要得功能
    private final Sync sync = new Sync();

    /***
     * 调同步器的模板方法,模板方法再调重写的拿锁方法
     * 该方法一定会拿到同步状态,阻塞式
     */
    @Override
    public void lock() {
        sync.acquire(1);
    }


    /***
     * 尝试获取同步状态,非阻塞式,立即响应结果:拿到还是没拿到
     * @return 是否获取成功
     */
    public boolean tryLock(){
        return sync.tryAcquire(1);
    }

    /***
     * lock()和tryLock()都是独占式获取同步状态,都是调tryAcquire()
     * lock是先拿一次,拿不到就一直循环拿,直到能拿到,所以是阻塞式
     * tryLock是就拿一次,拿到拿不到都返回,所以是非阻塞式
     */

    /***
     * 释放同步状态
     * 跟上面获取是一个道理
     */
    @Override
    public void unlock() {
        sync.release(0);
    }
    public boolean tryUnlock() {
        return sync.tryRelease(0);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
    
    //使用
    public static void main(String[] args) {
        Mutex mutex1 = new Mutex();//创建自己的互斥锁
       Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                mutex1.lock();
                try {
                    System.out.println(name+":加锁成功,执行自己的逻辑");
                    Thread.sleep(2000);
                }catch (Exception e){
                }finally {
                  mutex1.unlock();
                }
                System.out.println(name+":释放锁");
            }
        },"Thread-1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                mutex1.lock();
                try {
                    System.out.println(name+":加锁成功,执行自己的逻辑");
                    Thread.sleep(2000);
                }catch (Exception e){
                }finally {
                    mutex1.unlock();
                }
                System.out.println(name+":释放锁");
            }
        },"Thread-2");
        t.start();
        t2.start();
    }
}

四、实现分析

4.1 、实现原理

​ AQS 内部维护了一个同步队列,用于管理同步状态:

  • 当线程获取同步状态失败时,就会将当前线程以及等待状态等信息构造成一个 Node 节点,将其加入到同步队列中尾部,阻塞该线程

  • 当同步状态被释放时,会唤醒同步队列中“首节点”的线程获取同步状态

    在这里插入图片描述

4.2 、源码分析

独占式:
关于源码理解分三步:
1、先尝试获取同步状态,调用重写的独占式获取方法。
2、获取失败后就构建node节点,安全的加入到队列的尾部。
3、忙循环方式一直等到获取同步状态。这一步主要工作就是循环方式给前驱节点下通知,通知成功后,该线程就暂停了,也不循环了,等前驱节点通知自己后再在循环里尝试获取同步状态

/***
     * 模板方法  需要在自定义方法里调用该模板方法
     */
    public final void acquire(int arg) {
        //① 尝试拿同步状态
        boolean ok = tryAcquire(arg);

        //② 获取失败的线程构建成node等待节点,安全的加入到队列尾部
        if(!ok){
            Node node = new Node(Thread.currentThread(), Node.EXCLUSIVE);
            Node pred = tail;
            boolean setTail = true;//标记设置成尾节点的状态
            //当队列不为空时才尝试加入到尾部
            if (pred != null && compareAndSetTail(pred, node)) {
                node.prev = pred;
                pred.next = node;
                setTail = false;//成功的加入到尾部了
            }
            //队列里一个节点都没有或者并发时没有成功加入尾部
            if(setTail){
                node.prev = pred;
                //采用循环的方式设置尾节点,结束条件是:node成功的设置成尾节点
                for (;;) {
                    Node t = tail;//每次循环都重新取一下尾节点
                    //还没有尾节点。可以理解成空队列
                    if (t == null) {
                        //建一个空节点,既是头又是尾,cas原子操作,保证多线程下只构建一个哨兵节点,有个哨兵节点后面的操作就会避免空指针
                        if (compareAndSetHead(new Node())) {
                            tail = head;//头和尾都是该哨兵节点,再次循环队列就不为空了
                        }
                    } else {
                        //队列不为空就尝试设置成尾节点,设置不成功就循环,直到成功设置
                        if (compareAndSetTail(t, node)) {
                            node.prev = t;
                            t.next = node;
                           break;
                        }
                    }
                }
            }
            //经过第二部操作,等待的线程已经成功加入到队列了,下面就该等着获取同步状态了

            //③ 等待最终获取到同步状态
            boolean failed = true;//标记整个流程是否出问题了
            boolean interrupted = false;//标记是否被中断过
            try {
                //忙循环设置前驱节点状态(给前驱节点下个通知:你用完了同步状态告诉我,我再用)
                for (;;) {
                    final Node p = node.predecessor();//找到前驱节点
                    //当前驱是头节点时就尝试再拿一次同步状态
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // 断开之前那个头节点(可能是哨兵节点),有利于gc回收
                        failed = false;
                        break;//获取成功后就跳出循环
                    }
                    //没有拿到同步状态就给前驱节点一个通知(设置成SIGNAL,表示自己用完同步状态后要通知后继节点),自己就挂起了(暂停状态)
                    int ws = pred.waitStatus;//拿到前驱线程的状态
                    if (ws == Node.SIGNAL){//如果前驱节点已经是SIGNAL状态了,没自己啥事可以被中断了,等着被unpark唤醒就行了。
                        // 同步状态释放的时候会有唤醒操作,唤醒后会继续这个循环,有机会获取同步状态了
                        LockSupport.park(this);//被park后就不在走下面了,直到被unpark
                        if(Thread.interrupted()){
                            interrupted = true;//标记成被中断了
                        }
                    }else if (ws > 0) {
                    //剔除被取消了的节点(这些节点可能因为中断或者超时等原因不再争夺同步状态了)
                        do {
                            node.prev = pred = pred.prev;
                        } while (pred.waitStatus > 0);//一直往前找,找到正常的(signal)的节点
                        pred.next = node;
                    } else {
                        //如果前驱节点正常并且不是SIGNAL状态就用cas安全的把前驱节点设置成SIGNAL状态
                        compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizerMini.Node.SIGNAL);
                    }
                    //节点的状态这块大体意思就是先看看是不是想要的状态,是:就挂起;不是再判断是不是被取消了,没取消就设置signal状态
                    //经过这个try块后最终会获取到同步状态
                }
            } finally {
                //如果在没成功获取同步状态之前抛异常了
                if (failed) {
                    //取消等待的这个节点,并且从这个尾节点往上找,找到第一个正常的节点设置成尾节点
                    cancelAcquire(node);
                }
            }
            if(interrupted){
                //如果它被要求中断过,自己再主动中断
                Thread.currentThread().interrupt();
            }
        }
    }
```java
/***
     * 队列同步器提供的:模板方法  需要在自定义方法里调用该模板方法
     * 独占式释放同步状态,主要分2步:
     * 1、调用重写的方法释放同步状态
     * 2、把自己节点状态设置为0,用unpark唤醒后继节点(等待队列中最靠前的那个正常节点)
     * @param arg
     */
    public final boolean release(int arg) {
        //调用子类重写的尝试获取同步状态的方法
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0){
//                unparkSuccessor(h); 以下就是该方法
                int ws = h.waitStatus;
                if (ws < 0){//节点是正常状态
                    compareAndSetWaitStatus(h, ws, 0);//释放同步状态了,就把该节点的状态设置成0
                }
                Node s = h.next;//后继节点
                //如果没有后继节点或者后继节点状态是取消了,从后往前找,直到找到后面的第一个正常的节点
                if (s == null || s.waitStatus > 0) {
                    s = null;
                    for (Node t = tail; t != null && t != h; t = t.prev) {
                        if (t.waitStatus <= 0) {
                            s = t;
                        }
                    }
                }
                if (s != null) {
                    LockSupport.unpark(s.thread);//唤醒后继节点
                }
                 return true;
            }
        }
        return false;
    }

共享式:

/***
 * 队列同步器提供的:模板方法  需要在自定义的方法里调用该模板方法,然后它在调用重写的同步器方法tryAcquireShared()
 * 共享式获取同步状态,整体过程分2步:
 * 1、尝试获取同步状态;
 * 2、获取失败就加入等待队列
 * @param arg
 */
public final void acquireShared(int arg) {
    //先判断是否成功获取了同步状态
    int state = tryAcquireShared(arg);
    if ( state< 0){
        //没有拿到同步状态,加入等待队列
        doAcquireShared(arg);
    }
}
/***
 * 共享式获取同步状态失败进入等待队列
 * @param arg
 */
private void doAcquireShared(int arg) {
    //创建共享节点,并加到队列的尾节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;//定义整个过程是否正常的标识
    try {
        boolean interrupted = false;//定义是否被中断的标识
        for (;;) {
            //获取当前节点的前驱节点
            final Node p = node.predecessor();
            //前驱节点是头节点,也就是只有老二才走下面的,其他的就挂起休息等待被通知就行了。
            if (p == head) {
                //尝试再次获取同步状态
                int r = tryAcquireShared(arg);
                //如果老二拿到了同步状态,
                if (r >= 0) {
                    //设置头并且传播下去:通知等待队列的线程,让他们尝试获取同步状态
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    //如果被标记为中断的,则可以进行线程中断了
                    if (interrupted) {
                        selfInterrupt();
                    }
                    failed = false;
                    return;
                }
            }
            //这两个方法跟独占式的功能一样,都是把前驱节点标记为Signal后自己挂起线程去休息,等着被通知就行了
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
                interrupted = true;
            }
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
/***
 * 设置头节点并传播下去
 * @param node 该节点是曾经的老二
 * @param propagate
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);//曾经的老二现在是扛把子了
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared()) {//如果后继节点是共享的,告诉后继节点它也可以尝试去拿同步状态了
            doReleaseShared();
        }
    }
}
/***
 * 队列同步器提供的:模板方法  需要在自定义方法里调用该模板方法
 * 共享式释放同步状态,主要分2步:
 * 1、调用重写的方法释放同步状态
 * 2、用unpark唤醒后继节点(等待队列中最靠前的那个正常节点)
 * @param arg
 */
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
/***
 * 共享模式释放同步状态  并且 往后传播下去(告诉后面得已经释放同步状态了)
 */
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //存在头节点并且它还有后继节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //如果头节点是SIGNAL状态,它需要唤醒后继节点了
            if (ws == Node.SIGNAL) {
                //把自己节点状态设置成0,如果设置失败就跳出本次循环,下一次循环在尝试设置
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    continue;
                }
                unparkSuccessor(h);//自己完全“没用”了,唤醒后继节点
            }
            else if (ws == 0 && !compareAndSetWaitStatus(h,0,Node.PROPAGATE)) {
                continue;                // loop on failed CAS
            }
        }
        if (h == head) {
            break;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值