扫描下方二维码或者微信搜索公众号
菜鸟飞呀飞
,便可关注微信公众号,阅读更多Spring源码分析
和Java并发编程
文章。编程
AbstractQueuedSynchronizer
,简称AQS,翻译过来就是抽象的队列同步器。从命名就能猜出,这个类是一个抽象类,且是基于队列来实现的一个同步器。JUC包下全部的锁都是基于它来实现的。在AQS中定义了一个int类型的变量:state
,用它来表示同步状态,哪一个线程成功对state变量进行了加1操做,那么这个线程就持有了锁;AQS中还定义了一个FIFO(先进先出)的队列
,用来表示等待获取锁的线程。入口等待队列
和条件等待队列
呢?答案是确定的。AQS中也存着两个队列:同步队列
和条件等待队列
,它们分别对应管程中的入口等待队列
和条件等待队列
。今天先分析AQS中的同步队列
的数据结构和实现原理,关于AQS中条件等待队列
会在Condition
类的源码分析中讲解。《Java并发编程实战》
一课中的第一部分第8讲:管程:并发编程的万能钥匙
【图】方法 | 做用 |
---|---|
protected boolean tryAcquire(int arg) | 独占式尝试获取锁 |
protected boolean tryRelease(int arg) | 独占式尝试释放锁 |
protected int tryAcquireShared(int arg) | 共享式尝试获取锁 |
protected boolean tryReleaseShared(int arg) | 共享式尝试释放锁 |
protected boolean isHeldExclusively() | 当前线程是否独占式的占用锁 |
尝试
二字,这是由于调用这些方法不必定能获取锁成功或者释放锁成功)方法 | 做用 |
---|---|
int getState() | 获取同步状态state的值 |
void setState(int newState) | 修改同步状态。一般是只有已经获取到锁的线程才调用这个方法去修改同步状态,这个时候由于只有一个线程能取到锁,因此不用担忧并发问题 |
boolean compareAndSetState(int expect, int update) | 经过CAS的方式去修改同步状态,当多个线程同时尝试修改state时使用,它能保证只有一个线程能修改为功 |
方法 | 做用 |
---|---|
void acquire(int arg) | 独占式获取同步状态,若是线程成功获取了同步状态,则方法会返回,若是没有获取到同步状态,那么当前线程就会进入到同步队列中,并阻塞。该方法对中断没法响应 |
void acquireInterruptibly(int arg) throws InterruptedException | 和acquire() 方法同样,不过该方法能响应中断 |
boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException | 在acquireInterruptibly() 方法的基础上增长了超时限制,当指定时间内若是没有获取到同步锁,就会返回false。 |
void acquireShared(int arg) | 共享式获取同步状态,若是线程成功获取到了同步状态,那么方法就会返回。不然进入到同步队列中进行等待,并阻塞。它与acquire() 的区别是,该方法能容许多个线程同时获取到锁 |
void acquireSharedInterruptibly(int arg) throws InterruptedException | 在acquireShared() 方法的基础上增长了响应中断的功能 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException | 在acquireSharedInterruptibly() 基础上增长了超时功能,在指定时间内若是没有获取到锁,就会返回false |
boolean release(int arg) | 独占式释放锁 |
boolean releaseShared(int arg) | 共享式释放锁 |
acquire()
方法和release()
方法便可,其余的方法与这两个方法的实现几乎同样,只是改变了部分逻辑。ReentrantLock
,后者的实际应用有ReentrantReadWriteLock、CountDownLatch、CyclicBarrier
等。这些类的源码以及实现原理后面会有文章专门分析。要想读懂AQS的源代码,首先须要明白它的设计原理,不然很难看明白其中的逻辑。毕竟代码只是具体实现的工具,编程语言能够多变,但设计原理是不变的。设计模式
同步状态
和同步队列
。同步状态由state这个int类型的全局变量实现,哪一个线程成功修改了state的值,就表示这个线程获取到了锁或者释放了锁。同步队列是一个遵循先进先出(FIFO)
的队列,它是一个由Node节点组成的双向链表。每个线程在获取同步状态时,若是获取同步状态失败,就会将当前线程封装成一个Node,而后将其加入到同步队列中。Node是AQS里面的一个静态内部类,Node这个数据结构中,包含了5个属性,每一个属性的功能以下列表。Node就是经过这5个属性来实现同步队列
和等待队列
的,关于等待队列今天先暂时不分析,后面在分析Condition
源码时会详细分析。属性名 | 做用 |
---|---|
Node prev | 同步队列中,当前节点的前一个节点,若是当前节点是同步队列的头结点,那么prev属性为null |
Node next | 同步队列中,当前节点的后一个节点,若是当前节点是同步队列的尾结点,那么next属性为null |
Node thread | 当前节点表明的线程,若是当前线程获取到了锁,那么当前线程所表明的节点必定处于同步队列的队首,且thread属性为null,至于为何要将其设置为null,这是AQS特地设计的。 |
int waitStatus | 当前线程的等待状态,有5种取值。0表示初始值,1表示线程被取消,-1表示当前线程处于等待状态,-2表示节点处于等待队列中,-3表示下一次共享式同步状态获取将会无条件地被传播下去 |
Node nextWaiter | 等待队列中,该节点的下一个节点 |
prev
属性和next
属性就构成了一个双向链表,也就是AQS中的同步队列,可是想要经过这个队列找到队列中的每个元素,咱们就须要知道这个队列的头结点是谁,尾结点是谁。所以AQS中又提供了两个属性:head
和tail
,这两个属性的类型均是Node类型,它们分别指向同步队列中的头结点和尾结点。这样AQS就能经过head和tail,找到队列中的每个元素。同步队列的结构示意图以下。acquire()
方法获取同步状态的时候,若是此时能成功获取到同步状态,那么就直接返回;若是不能获取到同步状态,此时就表示同步状态已经被其余线程获取到了,那么这个时候,当前线程就须要开始等待,那么如何实现等待呢?此时当前线程先现将本身封装成一个Node
,而后这个Node加入到同步队列中
。在加入到同步队列以前,须要判断队列有没有被初始化
,即队列中有没有节点存在。若是head=null
则表示当前同步队列尚未初始化
,因此这个时候当前线程作的第一件事,就是初始化队列。如何初始化呢?当前线程须要先初始化head节点,所以它会new一个Node,而后将这个Node赋值给head,注意head节点表示的是获取到同步状态的线程。接着当前线程再将本身封装成一个Node,而后将head的next属性指向这个Node
,这样就将本身加入到了队列中。注意head节点的thread属性始终都是null
,由于head节点是当前线程建立的,而当前线程只知道有线程获取到了同步状态,可是殊不知道是谁获取到了,因此此时当前线程在初始化head节点的时候,只能让head节点的thread属性为null。当前线程再将本身加入到队列以后,还须要将tail指向本身。在设置head属性和tail属性
时,因为存在多个线程并发的可能
,因此须要使用AQS提供的compareAndSetHead()、compareAndSetTail()
方法,这两个方法会调用Unsafe类的CAS方法,能保证原子性。节点加入到同步队列的示意图以下。因为AQS遵循FIFO
,因此此时线程在释放同步状态后还须要唤醒后面节点的线程去获取同步状态。当有线程获取到同步状态后,须要将本身表明的节点设置为同步队列的首节点。因为此时确定只有一个线程获取到同步状态
,所以此时在更新head属性时,不须要经过CAS方法来保证原子性
,只须要使用setHead()
方法便可。首节点的设置的示意图以下。