抽象队列同步器(独占锁)

  • 基础介绍
  • AQS代码概览
  • Node类解析
  • 通过ReentrantLock窥探AQS独占锁
    • 最简单的实例
    • 重入锁实例
    • 锁竞争实例
  • 结尾
基础介绍 JUC中的许多并发类都继承了AbstractQueuedSynchronizer(AQS),如CountDownLatch、ReentrantLock、ThreadLocalExcutor等。
抽象队列同步器(独占锁)
文章图片

它主要实现了对同步状态的管理以及对阻塞线程进行排队、等待通知,就拿ReetrantLock为例,它有以下的功能
  • 获取锁
  • 争抢这把锁却没有成功的这些线程要被存放到一个集合中
  • 释放锁,集合中的线程会被唤醒重现来争抢锁
  • 使用锁来创建Condition对象
  • .....
上述这写功能都是依赖于AQS实现的,因为ReetrantLock是只能被一个线程获取,所以它是一把独占锁,而像ReentrantReadWriteLock中的ReadLock是可以被多个线程共享的,也就是说它是一把共享锁。AQS中既提供了独占锁的一些底层的实现,也提供了共享锁的实现。
所以AQS中的内容主要可以分为四部分
  1. CLH队列:存储等待线程,其主要是通过双向链表的方式实现,CLH是它的发明者的三个大佬的名字的首字母。
  2. 独占锁
  3. 共享锁
  4. Condition实现
AQS代码概览 抽象队列同步器(独占锁)
文章图片

AbstractQueuedSynchronizer这个类当中包含两个内部类,其中ConditionObject就是Condition功能的主要实现,一般创建Condition的方式就是Lock.newCondition(),而我们通过查看ReentrantLock源码可以发现,其实际创建的Condition就是一个ConditionObject实例。
Node是等待线程的载体,也就是等待线程所在的双向链表上的节点。
抽象队列同步器(独占锁)
文章图片

抽象队列同步器(独占锁)
文章图片

AbstractQueuedSynchronizer中有大量的方法,其中类似于tryAcquire和tryAcquireShared就是"类似方法"在独占锁和共享锁中的不同实现。
抽象队列同步器(独占锁)
文章图片

下图中是AbstractQueuedSynchronizer中的一些成员变量,其中head和tail都是一个Node变量分别用于表示队头和队尾节点,state表示同步状态,stateOffset表示state变量相对于java对象的偏移量,也就是相对于AbstractQueuedSynchronizer.class的偏移量(class也是一个对象,java中万物皆对象),主要是用于后面使用CAS的方式给相应变量设置值、修改值等操作,headOffset、tailOffset同理,waitStatusOffset和nextOffset是相对于Node.class的偏移量。另外在AbstractQueuedSynchronizer 的父类AbstractOwnableSynchronizer中还有一个重要的变量exclusiveOwnerThread表示独占模式下拥有当前锁的线程。
抽象队列同步器(独占锁)
文章图片

抽象队列同步器(独占锁)
文章图片

Node类解析
static final class Node { //用于标记共享模式 static final Node SHARED = new Node(); //用于标记独占模式 static final Node EXCLUSIVE = null; // waitStatus 为这个值的时候 表示线程已经被取消 static final int CANCELLED =1; // waitStatus 为这个值的时候 表示后继线程需要取消阻塞 static final int SIGNAL= -1; // waitStatus 为这个值的时候 表示线程处于Condition下的等待状态 static final int CONDITION = -2; //waitStatus 为这个值的时候 表示下个acquireShared操作将被允许 static final int PROPAGATE = -3; /** *这个字段可能有以下状态 *SIGNAL:该节点的后继节点被(或即将)阻塞(通过停放),因此当前节点在 *释放或取消时必须解除其后继节点的停放。为了避免竞争,获取方法 *必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时 *阻塞。 *CANCELLED:节点因超时或中断而被取消 *CONDITION:这个节点被用于condition队列,在装个状态下这个节点不会被用于 *同步队列。 *PROPAGATE:这个节点是被共享的 *0:以上都不是 * *这个字段的初始值为0,且是通过cas的方式对他进行安全写操作 */ volatile int waitStatus; //前置节点 volatile Node prev; //继承节点 volatile Node next; //该节点拥有的线程 volatile Thread thread; //可能有两种作用 //1.独占模式下的condition条件下的等待节点 //2.用于判断是共享模式 Node nextWaiter; //是否是共享模式 final boolean isShared() { return nextWaiter == SHARED; }//返回前置节点 final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; }Node() {// Used to establish initial head or SHARED marker }Node(Thread thread, Node mode) {// Used by addWaiter this.nextWaiter = mode; this.thread = thread; }Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }

通过ReentrantLock窥探AQS独占锁 下面我们通过几个实例来探究AQS中的一些方法的实现以及在ReentrantLock中起到的作用。
最简单的实例
下面我们就通过一个简单的lock & unLock实例来入手
抽象队列同步器(独占锁)
文章图片

通过断点进入,lock的具体实现在ReentrantLock的NonfairSync内部类中,这是由于我们为lock对象设置的非公平锁。
抽象队列同步器(独占锁)
文章图片

抽象队列同步器(独占锁)
文章图片

然后我们会进入AQS中的compareAndSetState方法,它主要是通过cas的方式判断state是否为0,是-就将其更改为1并返回true、否-不修改直接返回false,若为0就意味着这个锁现在是没有被任何线程占有的,然后我们将它的状态更改为1表示将其占用。
抽象队列同步器(独占锁)
文章图片

返回是后,将AQS中的独占线程的字段赋值为当前线程,然后就加锁成功了。
抽象队列同步器(独占锁)
文章图片

然后我们进入ReentrantLock的unlock方法,这个方法的主要实现就是在AQS的release方法中
抽象队列同步器(独占锁)
文章图片

然后进入tryRelease方法,会获取当前锁的状态,然后用c表示要被更改成的目标状态,校验后,将锁的独占线程置为空并修改其状态字段state为目标状态,到这里锁已经解除占用了。
抽象队列同步器(独占锁)
文章图片

tryRelease返回为true后会判断AQS中存不存在等待节点,如果存在则就将其唤醒(后面会看这里的源码)
重入锁实例
我们使用同一把ReentrantLock进行两次lock操作,由于第一次和上面的简单实例流程是一样的所以我们只关注第二次lock和unLock
抽象队列同步器(独占锁)
文章图片

此时,由于已经lock过一次,即state=1,所以compareAndSetState(0,1)不会赋值成功,所以会进入到acquire方法,进而会首先进入到tryAcquire方法
抽象队列同步器(独占锁)
文章图片

我们会在TryAcquire中再次判断锁的状态(因为在此过程中上一次lock可能被释放),然后由于当前线程就是这把锁的独占线程,所以我们是可重入这把锁的,最后将state的值改为2代表这把锁被当前线程重入了两次。
抽象队列同步器(独占锁)
文章图片

由于tryAcquire(1)返回的是true所以!tryAcquire(1)为false导致程序不会进入acquire方法中的后续执行流程,到此,意味着第二次lock已经完成。
和简单实例中的unlock一样,程序会先进入release方法然后进入tryRelease方法,再这里面因为更改后的state为1所以不会讲当前锁的独占线程设置为null(会在最后一次unlock中设置)
抽象队列同步器(独占锁)
文章图片

锁竞争实例
锁竞争就会涉及到等待队列以及等待节点的阻塞与唤醒,所以它的一系列操作的复杂度相对于上面的例子要更高一些。使用以下实例来体验一下多线程竞争锁的过程。
t1会首先获取到lock,这过程与无竞争锁的获取是一样的,主要的不同点在于t2获取锁和t1释放锁的过程。
抽象队列同步器(独占锁)
文章图片

在idea中可以在下入这个位置切换调试的线程
抽象队列同步器(独占锁)
文章图片

在t1线程获取到锁之后,我们切换到t2线程,发现idea此时已经给我们标注了lock这把锁已经被t1占用了。
抽象队列同步器(独占锁)
文章图片

然后会进入到acquire方法,由于此时t1已经占用了锁,所以state ≠ 0且拥有锁的当前线程为t1≠t2所以 tryAcquire返回的是false,因此程序会进入addWaiter方法。
抽象队列同步器(独占锁)
文章图片

抽象队列同步器(独占锁)
文章图片

在这个方法中,会首先将t2线程封装到一个Node对象当中,然后通过tail节点判断队列是否被初始化了,由于CLH队列此时并没有元素存在,所以会进入到enq方法进行队列的首次初始化。
抽象队列同步器(独占锁)
文章图片

在enq中会初始化这个队列会初始化队列,然后将传入的node插入到队尾,在这里面我们看到了for(; ; )的死循环(优雅点可以叫做自旋),那么它的作用是什么呢?
抽象队列同步器(独占锁)
文章图片

在此次进入到enq中实际上for只进行了两次,第一次给头节点设置了一个没有实际数据的head节点,第二次将传入的node加入到了队尾,那么以上工作我们是可以在一次循环中完成的,就比如以下代码块中的实现方式
private Node enq(final Node node) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } }

其实自旋是为了保证线程安全,在t2线程获取锁的时候可能也有其它线程正在争抢lock,就比如恰好有线程在t2执行Node t = tailcompareAndSetHead(new Node())之间的时候初始化成功了队列设置了head节点, 那么compareAndSetHead就会返回false不会进入这个分支,这时候就会重新获取tail节点再将传入的node节点插入到tail地next中,但是,此时tail可能也会被别的线程更改,那么就需要不断地自旋尝试修改直到成功位置,自旋结束。
addWaiter结束后会进入acquireQueued,这个方法主要是会进行锁地争抢以及阻塞等待,最后根据failed字段判断是否要取消获取线程,这种情况一般就是状态被置为了Canceled
抽象队列同步器(独占锁)
文章图片

shouldParkAfterFailedAcquire判断节点是否应该阻塞等待,如果这个节点为SIGNAL状态就说明该节点的后继节点应该被阻塞,继而会执行parkAndCheckInterrupt方法对其进行阻塞,并在它被唤醒的时候判断此线程是否是被interrupt的。
抽象队列同步器(独占锁)
文章图片

正常情况下如果如果t1线程不unlock,那么t2线程将一直阻塞在parkAndCheckInterrupt方法,当其被唤醒后会继续自旋尝试获取锁。
然后我们切换回t1线程,进入unlock方法,调用AQS的release方法,然后tryRelease里面操作跟上面两个实例相同不在赘述,唯一不同的是之前实例的等待队列都为空,也就是head节点都是null,所以不会去唤醒阻塞节点,因为此时我们有t2线程所在的节点是被存储到了队列中所以,程序会进入到unparkSuccessor方法中,执行完这个方法后t2线程会从之前的WAIT状态转换为RUNNING状态即被唤醒!
抽象队列同步器(独占锁)
文章图片

抽象队列同步器(独占锁)
文章图片

t2被唤醒后会去再次tryAcquire,成功后去执行临界区的内容,然后正常释放lock锁。
结尾 【抽象队列同步器(独占锁)】上面利用ReentrantLock介绍了AQS独占锁相关内容,除此之外,后面还会通过ReentrantReadWriteLock介绍共享锁的实现、Condition的实现以及其它相关的JUC类中AQS的使用。

    推荐阅读