背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方。
Java并发包基石-AQS详解
Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
基本实现原理
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
AQS支持两种同步方式:
1.独占式
2.共享式
这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。
同步器的设计是基于模板方法模式的,一般的使用方式是这样:
1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这其实是模板方法模式的一个很经典的应用。
我们来看看AQS定义的这些可重写的方法:
protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false
protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;
protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;
protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false
protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。
关于AQS的使用,我们来简单总结一下:
如何使用
首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。
设计思想
对于使用者来讲,我们无需关心获取资源失败,线程排队,线程阻塞/唤醒等一系列复杂的实现,这些都在AQS中为我们处理好了。我们只需要负责好自己的那个环节就好,也就是获取/释放共享资源state的姿势T_T。很经典的模板方法设计模式的应用,AQS为我们定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现即可。
AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。
AQS的源码的主要点在于node节点、共享式 独占式 阻塞队列,cas
源码分析
我们先来简单描述下AQS的基本实现,前面我们提到过,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。
其实就是个双端双向链表。
当线程获取资源失败(比如tryAcquire时试图设置state状态失败),会被构造成一个结点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有同步状态的线程释放同步状态时,会唤醒后继结点,然后此结点线程继续加入到对同步状态的争夺中。
ps:此处可以参考
(转)分布式系统互斥性与幂等性问题的分析与解决
Node结点
Node结点是AbstractQueuedSynchronizer中的一个静态内部类,我们捡Node的几个重要属性来说一下
static final class Node { /** waitStatus值,表示线程已被取消(等待超时或者被中断)*/ static final int CANCELLED = 1; /** waitStatus值,表示后继线程需要被唤醒(unpaking)*/ static final int SIGNAL = -1; /**waitStatus值,表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中 */ /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** waitStatus值,表示下一次共享式同步状态会被无条件地传播下去 static final int PROPAGATE = -3; /** 等待状态,初始为0 */ volatile int waitStatus; /**当前结点的前驱结点 */ volatile Node prev; /** 当前结点的后继结点 */ volatile Node next; /** 与当前结点关联的排队中的线程 */ volatile Thread thread; /** ...... */ }
ps:static final 和volatile变量
独占式
获取同步状态--acquire()
至此,关于acquire的方法源码已经分析完毕,我们来简单总结下
a.首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;
b.线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;
c.加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
ps:多个线程都通过【CAS+死循环】这个free-lock黄金搭档来对队列进行修改,每次能够保证只有一个成功,如果失败下次重试,如果是N个线程,那么每个线程最多loop N次,最终都能够成功。
释放同步状态--release()
当前线程执行完自己的逻辑之后,需要释放同步状态,来看看release方法的逻辑
release的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。
共享式
共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。
ps:共享式和独占市的区别。共享式tryAcquire返回boolean,tryAcquireShared返回int,分一下集中
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;
2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;
3.当返回值小于0时,表示获取同步状态失败。
获取同步状态--acquireShared
代码逻辑比较容易理解,需要注意的是,共享模式,释放同步状态也是多线程的,此处采用了CAS自旋来保证。
总结
关于AQS的介绍及源码分析到此为止了。
AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。
AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法的应用。AQS定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现。