AQS(AbstractQueuedSynchronizer)

AQS(AbstractQueuedSynchronizer)

AQS: 是一个用来构建锁和同步器的框架,使用AQS 能简单且高效的构造出应用广泛的大量同步器,比如(ReentrantLockSemaphore

在这里插入图片描述

AQS

AQS 是一个FIFO的双向队列,内部通过节点headtail记录队首和队尾元素,队列元素的类型为Node

state:单一的状态信息;

  1. 对于ReentrantLock而言,state表示当前线程获取锁的可重入次数。(当一个线程获取到ReentrantLock锁后,在AQS内部首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会改变state的值由1变为2

  2. 对于ReentrantReadWriteLock而言,state高16位表示读状态(读锁的次数),低16位表示获取到写锁的线程的可重入次数

  3. 对于Semaphore而言,state表示当前可用信号的个数

  4. 对于CountDownlatch而言,state表示计数器当前的值

对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

独占式

独占方式下,获取和释放资源的流程如下。

只有一个线程能执行,如 ReentrantLock 。又可分为公平锁和非公平锁

/**
 * 首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,
 * 成功则直接返回,失败则将当前线程封装为类型为
 * Node.EXCLUSIVE的Node节点通过addWaiter()方法插入到AQS阻塞队列的尾部,
 * 最后调用acquireQueued(Node node,int arg)方法,使得该节点以死循环的方式
 * 获取同步状态,如果获取不到则调用LockSupport.park(this)方法挂起自己,
 * 被挂起的线程唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现
 */ 
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
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);
    }
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
/**
 * 首先尝试使用tryRelease操作释放资源,这里是设置状态变量state的值。
 * 然后调用 LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)
 * 被激活的线程则使用tryAcquire尝试,看当前状态变量state是否能满足自己的需要,
 * 满足则线程被激活,然后继续运行,否则放入AQS队列挂起
 */ 
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

独占式同步状态获取流程,也就是acquire(int arg)方法获取同步状态,如下图

在这里插入图片描述

共享式

多个线程可以同时执行。SemaphoreCountDownLatchCyclicBarrierReadWriteLock

共享方式下,获取和释放资源的流程如下。

//首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值。
//成功则直接返回,失败则将当前线程封装为类型Node.SHARED的Node节点后插入
//到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
//尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用 //LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)
//被激活的线程则使用tryAcquire尝试,看当前状态变量state是否能满足自己的需要,
//满足则线程被激活,然后继续运行,否则放入AQS队列挂起
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
入队源码解析

当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到AQS的阻塞队列;

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;//(代码1)
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))//(代码2)
                    tail = head;
            } else {
                node.prev = t;//(代码3)
                if (compareAndSetTail(t, node)) {//(代码4)
                    t.next = node;
                    return t;
                }
            }
        }
    }

接下来,我们通过节点图和代码来讲解一下入队的过程;

  1. 第一次循环:当要在AQS队列尾部插入元素时,AQS队列状态如下图中的(default)所示。也就是队列头、尾结点都指向null;当执行代码1后节点t指向了尾部节点,这时候队列状态如下图中(I)所示。这时候tnull,故执行代码2,使用CAS算法设置一个哨兵节点为头节点,如果CAS设置成功。则让尾结点也指向哨兵节点,这时候队列状态如下图的(II)所示。到此只插入了一个哨兵节点,还需要插入node节点;
  2. 所以第二次循环后执行到代码1,这个时候队列状态如下图(III)所示;然后执行代码3设置node的前驱节点为尾部节点,这个时候队列状态如下图(IV)所示,然后通过CAS算法设置node节点为尾部节点,CAS成功后队列状态如下图中(V)所示;CAS成功后再设置原来的尾部节点的后驱节点为node,这时候就完成了双向链表的插入,此时队列状态如下图的(VI)所示;

在这里插入图片描述

acquireQueued() 方法解析
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;
            }
            //判断是否应该park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自身观察,当条件满足,获取到了同步状态,皆可以从这个自旋过程中退出,否则依旧留在这个自旋的过程中。

if (p == head && tryAcquire(arg)) 可以看到只有前驱节点是头节点才会去获取同步状态;

shouldParkAfterFailedAcquire
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取上一个节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //上一个节点都在阻塞,那么自己也阻塞好了
            return true;
        //>0表示取消状态
        if (ws > 0) {
            //上一个节点取消,那么重构删除前面所有取消的节点,返回到外层循环重试
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //这次还没有阻塞
            //但下次如果重试不成功,则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL	
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
AQS的实现

需要注意的是,对于独占锁tryAcquiretryRelease。共享锁tryAcquireSharetryReleaseShareAQS类都没有提供可用的实现。需要具体的子类来实现。子类要根据具体场景使用CAS算法修改state状态值,成功则返回true,失败返回false。子类还需要定义,在调用acquirerelease方法时state状态值的增减代表什么含义;

正如前面介绍的state状态信息在不同子类中代表的不同含义;

AQS实现的锁除了需要重写上面介绍的方法外,还需要重写isHeldExclusively方法,来判断锁是被当前线程独占还是被共享;

所以当我们想要自定义一个同步器,需要实现的步骤就很清晰了;

AQS 的底层是 使用的 模板方式 设计模式,使用者需要继承同步器并重写指定的方法;

创建一个自定义的同步器:

  1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(共享资源state的获取与释放)
  2. AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法

AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法

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

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

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,stateCAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后续动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

自定义同步组件;

同一时刻只允许一个线程占有(不可重入的独占锁)


//一次只能一个线程访问
public class AQSDemo1 implements Lock {
    private static class Sync extends AbstractQueuedSynchronizer{
        //是否处于占用
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }
        //状态为0时获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        //释放锁状态设置为0
        @Override
        protected boolean tryRelease(int arg) {
            if (getState()==0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        //提供条件变量接口
        Condition newCondition(){return new ConditionObject();}
    }
    //仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

上述案例,通过调用同步器的acquire(int args)等模板方法,大大降低了实现一个自定义同步组件的门槛;

自定义同步组件TwinsLock

同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞;

确定访问模式:同一时刻支持多个线程访问,显然是共享式,需要使用acquireShared(int args)等系列方法;

定义资源数:至多两个线程同时访问,表明同步资源数为2.设置初始status为2,当一个线程进行获取,status减1。状态的合法范围为(0,1,2)当显示为0时,表示两个线程已经获取了同步资源,此时其他的线程只能阻塞;

//同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞;
public class TwinsLock implements Lock {
    private final Sync sync = new Sync(2);
    private static final class Sync extends AbstractQueuedSynchronizer{
        Sync(int count){
            if(count<=0){
                throw new IllegalArgumentException("count must large than zero");
                setState(count);
            }
        }
        @Override
        protected int tryAcquireShared(int reduceCount) {
            for(;;){
                int current = getState();
                int newCount = current - reduceCount;
                if(newCount < 0 || compareAndSetState(current,newCount)){
                    return newCount;
                }
            }
        }
        @Override
        protected boolean tryReleaseShared(int returnCount) {
        	for(;;){
                int current = getState();
                int newCount = current + returnCount;
                if(compareAndSetState(current,newCount)){
                    return true;
                }
            }   
        }
    }
    //仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    @Override
    public void lock() {
        sync.acquireShared(1);
    }
	@Override
    public void unlock() {
        sync.releaseShared(1);
    }
//其他接口方法略
}

测试demo

public class TwinsLockTest{
    @Test
    public void test(){
        final Lock lock = new TwinsLock();
        //启动10个线程
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.setDaemon(true);
            w.start();
        }
        //每隔一秒换行
        for (int i = 0; i < 10; i++) {
            SleepUtils.second(1);
            System.out.println();
        }
        
        class Worker extends Thread{
            @Override
            public void run() {
                while(true){
                    lock.lock();
                    try{
                        SleepUtils.second(1);
                        System.out.println(Thread.currentThread().getName());
                        SleepUtils.second(1);
                    }finally{
                        lock.unlock();
                    }
                }
            }
        }
    }
}

引用

《Java并发编程之美》
《Java并发编程的艺术》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王叮咚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值