抽象同步队列AQS原理和实践

AQS简述

AQS是一个FIFO的双向队列,队列元素类型为Node(也就是Thread)。AQS有一个state属性,ReentrantLock可以用来便是当前线程获取锁的可重入次数;对于samaphore来说,state表示当前可用信号的个数;对于CountDownLatch,state表示计数器当前的值。

AQS的内部类ConditionObject,它是条件变量,每个条件变量对应一个条件队列(单向链表),用来存放await方法后被阻塞的线程。

对于AQS来说,线程同步的关键是对状态值state操作。操作state的方式有独占和共享。使用独占方式获取的资源与线程绑定,一个线程获取了资源,会标记这个线程获取到了,其他线程想获取,发现获取资源的不是自己,就会陷入阻塞状态。

共享方式的资源与具体线程是不相关的,比如Semaphore信号量,当一个线程通过acquire获取信号量时,会看信号量个数是否满足,不满足就把线程放入阻塞队列,如果满足通过CAS获取信号量。

AQS的内部队列

AQS是JUC提供的一个用于构建锁和同步容器的基础类。例如ReentrantLock、Semaphore、CountDownLatch、FutureTask等都是基于AQS构建的。AQS解决了实现同步容器时设计的大量细节问题。

AQS是CLH队列的一个变种。AQS队列内部维护的是一个FIFO的双向链表,每个节点有前驱结点和后继节点。每个节点由线程封装,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

image

AQS的核心成员

AQS出于“分离变与不变”(单一职责和开闭原则)的原则,基于模版模式实现。AQS为锁获取、锁释放的排队和出队过程提供了一系列的模版方法。由于JUC的显式锁种类丰富,因此AQS将不同锁的具体操作抽取为钩子方法,让各种锁的子类去实现。

状态标志位

AQS中维持了一个单一的volatile变量state,state表示锁的状态。

private volatile int state;

state保证了可见性,所以任何线程通过getState()获取状态都可以得到最新值。AQS提供了compareAndSetState()方法利用底层UnSafe的CAS机制来实现原子性。

protected final boolean compareAndSetState(int exepect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

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

AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个基类只有一个变量exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了get和set方法。

队列节点类

Node

FIFO双向同步队列

每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部。当有线程释放锁时,AQS会尝试让队头的后继节点占用锁。

JUC显式锁与AQS的关系

AQS是一个同步器,它实现了锁的基本抽象功能,该类是由模版模式来实现的。

1.ReentrantLock与AQS的组合关系

ReentrantLock是一个可重入的互斥锁,可以被单个线程多次获取。ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:

static abstract class Sync extends AbstractQueuedSynchronizer { ... }

ReentrantLock支持公平锁和非公平锁。默认情况下是非公平锁。

final static class NonfairSync extends Sync { ... }
final static class FairSync extends Sync { ... }

由ReentrantLock的lock和unlock的源码可以看到,它们只是分别调用了sync对象的lock和release方法。

public void lock() {
    sync.acquire(1);
}

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

而Sync内部类只是AQS的子类,所以本质是ReentrantLock的操作是委托给AQS完成的。

AQS的模版流程

AQS定义了两种资源共享方式:

  • Exclusive(独享锁):只有一个线程能占有锁资源,如ReentrantLock。
  • share(共享锁):多个线程可以同时占有资源,如Semaphore、CountDownLatch。

AQS为不同的资源共享方式提供了不同的模版流程,AQS提供了一种实现阻塞锁和依赖FIFO等待队列的同步器的框架。自定义的同步器只需要实现共享资源state的获取与释放方式即可,这些逻辑都编写在钩子方法中。无论是共享锁还是独占锁,AQS在执行模版流程时都会回调自定义的钩子方法。

AQS的钩子方法

自定义同步器时,AQS中需要重写的钩子方法如下:

  • tryAcquire(int):独占锁钩子,尝试获取资源,若成功则返回true,若失败则返回false。
  • tryRekease(int):独占锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。
  • tryAcquireShared(int):共享锁钩子,尝试获取资源,负数表示失败;
  • isHeldExclusively():独占锁钩子,判断该线程是否正在独占资源。只有用到condition条件队列时才需要去实现它。

通过AQS实现简单的独占锁

image

SimpleMockLock只实现了Lock接口的两个方法:

(1)lock方法:完成显式锁的抢占。

(2)unlock方法:完成显式锁的释放。

SimpleMockLock的锁抢占和释放是委托给Sync实例的方法来实现的。在抢占锁时,AQS的acquire会调用tryAcquire钩子方法;释放锁时,AQS的release会调用tryRelease钩子方法。

内部类Sync继承AQS类时提供了一下两个钩子方法的实现:

(1)tryAcquire:将state设置为1并保存当前线程,表示互斥锁已经占用。

(2)tryRelease:将state设置为0,表示互斥锁已经被释放。

public class SimpleMockLock implements Lock {

    // 同步器实例
    private final Sync sync = new Sync();

    // 自定义的内部类:同步器
    // 直接使用 state 表示锁的状态
    // state = 0 表示锁没有被占用
    // state = 1 表示已经被占用
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 接下来不需要使用CAS操作,因为下面的操作不存在并发场景
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

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

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

    @Override
    public Condition newCondition() {
        return null;
    }

    static int i = 0;

    public static void lockAndFastIncrease(Lock lock) {
        lock.lock();
        i++;
        System.out.println(i);
        lock.unlock();
    }

    public static void main(String[] args) {
        LongAdder cnt = new LongAdder();
        final int TURNS = 1000;
        final int THREADS = 10;
        final ExecutorService pool = Executors.newFixedThreadPool(THREADS);
        final SimpleMockLock lock = new SimpleMockLock();
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREADS; i++) {
            pool.submit(() -> {
                try {
                    for (int j = 0; j < TURNS; j++) {
                        lockAndFastIncrease(lock);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        final long l = System.currentTimeMillis() - start;
        System.out.println("耗时:" + l);
        pool.shutdown();
    }

}

AQS锁抢占原理

流程的第一步,显式锁的lock方法会调用同步器基类AQS的模版方法acquire。acquire是AQS封装好的获取资源的公共入口,它是AQS提供的利用独占的方式获取资源的方法,源码如下

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

acquire至少执行一次tryAcquire钩子方法,tryAcquire默认抛出一个异常,具体的获取独占资源state的逻辑需要钩子方法来实现。若调用tryAcquire尝试成功,则acquire将直接返回,表示抢到锁;若不成功,则将线程加入等待队列中。

tryAcquire流程:CAS操作state字段,将值从0改为1,若成功表示锁未被占用,返回true;若失败,则返回false。如果是重入锁,state字段值会累积,表示重入次数。

直接入队:addWaiter。在acquire模版方法中,如果钩子方法tryAcquire返回失败,就构造同步节点(独占式节点模式为Node.EXCLUSIVE),通过addWaiter方法将节点加入同步队列的队尾。

自选入队:enq。addWaiter第一次尝试在尾部添加节点失败,意味有并发抢锁发生,需要自旋。enq方法通过CAS自旋将节点添加到队列尾部。

自旋抢占:acquireQueued。节点入队之后,启动自旋锁的流程,acquireQueued的主要逻辑:当前Node节点线程在死循环中不断获取同步状态,并且在前驱结点上自旋,只有当前驱结点是头结点时才尝试获取锁。为了不浪费资源,如果头结点获取了锁,那么该节点会终止自旋,线程回去执行临界区的代码。其余处于自旋状态的线程当然也不会自旋浪费资源,而是被挂起进入阻塞状态。

Re

《Java高并发核心编程》

和《Java并发编程之美》结合一起看比较好。

posted @ 2021-10-20 23:02  csgopher  阅读(143)  评论(0编辑  收藏  举报