并发编程之Lock接口与AQS(AbstractQueuedSynchronizer)原理设计

264 阅读9分钟

关于并发的jdk源码结构如下:

image.png

从整体上来看concurrent包的整体实现图如下图所示:

今天主要来学习下关于lock以及AQS的实现:

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

使用方式:

Lock lock = new ReentrantLock(); lock.lock();
try {
} finally {
  lock.unlock();
}

使用注意:
1.在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。避免锁泄露。 2.避免在try中加锁,如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。 与synchronized区别:

然后我们可以来看下lock中的几个api:

队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态在抽象方法的实现过程中免不了要对同步状态进行更改,主要使用以下三种方法进行更改:getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

子类比较推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。

与继承了Lock接口的不同锁之间的关系:

锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

在AQS有一个静态内部类Node,其中有这样一些属性:

volatile int waitStatus //节点状态
volatile Node prev //当前节点/线程的前驱节点
volatile Node next; //当前节点/线程的后继节点
volatile Thread thread;//加入同步队列的线程引用
Node nextWaiter;//等待队列中的下一个节点

节点的状态有以下这些:
int CANCELLED =  1//节点从同步队列中取消
int SIGNAL    = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;
int CONDITION = -2//当前节点进入等待队列中
int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去
int INITIAL = 0;//初始状态

现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是一个双向队列。

AQS的模板方法设计模式

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。 例如: AQS中需要重写的方法tryAcquire

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

ReentrantLock

public class ReentrantLock implements Lock, java.io.Serializable {
/** 同步器提供所有实现机制* /私有最终同步同步*/
    private final Sync sync;
  abstract static class Sync extends AbstractQueuedSynchronizer {} 实现该同步器

ReentrantLock中NonfairSync(继承AQS)会重写该方法为:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

而AQS中的模板方法acquire():

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

会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式.

总结下以下几点:

1.不同的锁实现主要通过继承Lock,并且设计一个相关依赖的静态内部类,sync(实现AQS)作为实现同步的组件。

2.AQS采用了模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法。

3.AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作

4.在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态。

同步器可重写的方法与描述如表:

实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下

列举一些设计思路:

ReentrantLock

比如可重入锁(ReentrantLock),根据是否公平调度分为两种实现,内部有两种实现AQS的同步器:这里忽略重入性等。

static final class NonfairSync extends Sync
static final class FairSync extends Sync

sync: abstract static class Sync extends AbstractQueuedSynchronizer

AQS主要是提供的getState(),setState(),compareAndSetState()方法进行修改同步状态,根据不同锁的特点,负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,来形成不同的锁,具备的特征。

再比如读写锁,实现了获取读锁时,可以重写tryAcquireShared,获取写锁时,还是重写tryAcquire。

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

我们也可以自己来实现一个:

public class Mutex implements Lock{

    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer{

        // 是否处于占用状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        @Override
        protected boolean tryAcquire(int arg) {
            //如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新*值。 
            //*此操作具有{@code volatile} read *和write的内存语义。
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int releases) {
            if(getState() == 0){ throw new IllegalMonitorStateException();}
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个condition,每个condition都包含了一个condition实例
        Condition newCondition() { return new ConditionObject(); }
    }

    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    @Override
    public void lock() { sync.acquire(1); }
    @Override
    public boolean tryLock() { return sync.tryAcquire(1); }
    @Override
    public void unlock() { sync.release(1); }
    @Override
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

测试如下:

public static void main(String[] args){
       // CountDownLatch countDownLatch = new CountDownLatch(1);
        Mutex mutex = new Mutex();
      //  countDownLatch.await();
        for(int i=0;i<10;i++){
            new Thread(() -> {
                    mutex.lock();
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    mutex.unlock();
                }
                }).start();
        }

结果如图所示:

按照推荐的方式,Mutex定义了一个继承AQS的静态内部类Sync,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。

在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。

总结: 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法; 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。

同步组件实现者的角度: 通过可重写的方法:独占式: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。

同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。

AQS的角度

而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。

整理不易,喜欢点个赞