高并发学习之10AQS的理解

1. AQS简介

在上一篇锁的认识中我们简单介绍了一下锁,已经锁的基本使用方式,和锁的两种重要的实现类ReentrantLock(重入锁)ReentrantReadWriteLock(读写锁) 的简单使用,在本篇中,将详细探讨其实现的关键步骤AQS(AbstractQueuedSynchronizer)队列同步器
AQS是用来构建锁或者其他同步组件的基础框架,看看我们熟悉的ReentrantLock其源码:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

  
    abstract static class Sync extends AbstractQueuedSynchronizer {
    	.....
    }
}

可以看到内部定义了一个抽象类Sync ,实现了AbstractQueuedSynchronizer 。在看下ReentrantReadWriteLock其源码:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
		abstract static class Sync extends AbstractQueuedSynchronizer {
			.....
		}
}

一样的,其内部也有个抽象类Sync 继承了AbstractQueuedSynchronizer 。
Java中AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
AQS是Java中实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合AQS,利用AQS实现锁的语义。
可以这样理解二者之间的关系:

  • 锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
  • 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理线程的排队等待与唤醒等底层操作。

锁和AQS很好地隔离了使用者和实现者所需关注的领域,即使用锁的人员只需要关注某个类型的锁他的API是什么,怎么使用,而锁的开发人员需要关注锁该怎么实现,提供什么API。

从使用上来说,AQS的功能可以分为两种:独占和共享。

  • 独占锁模式下,每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁
  • 共享锁模式下,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。

很显然,独占锁是一种悲观保守的加锁策略,它限制了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

2. AQS简单事例

AQS是提供给锁的开发人员的一种工具,那么我们现在就利用AQS简单开发一个自己的锁。但是在开发之前我们需要去了解AQS为我们定义了哪些方法。
AQS的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态
    设置的原子性。
    AQS可重写的方法与描述如下图所示:
    AQS可重写的方法
    实现自定义同步组件时,将会调用AQS提供的模板方法,这些(部分)模板方法与描述:
void acquire(int arg);// 独占式获取同步状态,如果获取失败则插入同步队列进行等待;
void acquireInterruptibly(int arg);// 与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
boolean tryAcquireNanos(int arg, long nanosTimeout);// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;
boolean release(int arg);// 释放同步状态,该方法会唤醒在同步队列中的下一个节点


void acquireShared(int arg);// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;
void acquireSharedInterruptibly(int arg);// 在acquireShared方法基础上增加了能响应中断的功能;
boolean tryAcquireSharedNanos(int arg, long nanosTimeout);// 在acquireSharedInterruptibly基础上增加了超时等待的功能;
boolean releaseShared(int arg);// 共享式释放同步状态

同步器提供的模板方法基本上分为3类:

  • 独占式获取与释放同步状态
  • -共享式获取与释放
  • 同步状态和查询同步队列中的等待线程情况。

自定义同步组件将使用AQS提供的模板方法来实现自己的同步语义。只有掌握了AQS的工作原理才能更加深入地理解并发包中其他的并发组件,所以下面我们开始写一个独占锁的示例来深入了解一下AQS的工作原理。

public class AQSDemo implements Lock {

    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -7590006332728291085L;

        // 返回一个Condition,每个condition都包含了一个condition队列,后面会分析condition,现在理解成队列就行
        Condition newCondition() {
            return new ConditionObject();
        }

        // 当状态为0的时候获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

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

    /******************* 下面都是实现lock的方法,所有方法的实现都是代理给AQS ************************/
    //获取锁
    @Override
    public void lock() {
        sync.acquire(1);
    }
    //获取锁的过程能够响应中断
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    //超时获取锁,在超时内或者未中断的情况下能够获取锁
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    //释放锁
    @Override
    public void unlock() {
        sync.release(1);
    }
    //获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}
	/******************* 测试是否达到线程安全 **************/
	private static AQSDemo aqsDemo = new AQSDemo();
    private static int count=0;
    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     aqsDemo.lock();
                     try {
                         count++;
                     } catch (Exception e) {
                         e.printStackTrace();
                     } finally {
                         aqsDemo.unlock();
                     }
                 }
             } ,"AQSDemo-"+i).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
输出结果:100

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

3. AQS内部分析

AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失时,AQS会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
Node的主要属性如下:

static final class Node {
  int waitStatus; //表示节点的5种状态
  Node prev; //前继节点
  Node next; //后继节点
  Node nextWaiter; //存储在condition队列中的后继节点
  Thread thread; //当前线程
}

其中节点状态说明:

  • Cancelled,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队
    列中取消等待,节点进入该状态将不会变化
  • Signal,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状
    态或者被取消,将会通知后继节点,使后继节点的线程得以运行
  • Condition,值为-2,节点在等待队列中,节点线程等待在Condition 上,当其他线程对
    Condition 调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状
    态的获取中
  • Propagate, 值为-3, 表示下一次共享式同步状态获取将会无条件地被传播下去
  • Initial, 值为0,初始状态

节点是构成同步队列(等待队列,后面文章中将会介绍)的基础,AQS拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。

通过上面AQSDemo例子中我们将i调整成10,dug调试如图:
AQS结构图
可以看出包含sync包含头尾两个节点属性,在看Node属性如图:
AQS结构图
每个节点都有prev(前置节点)、next(下一个节点),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态(waitStatus)等信息。
下面是AQS结构如图:
AQS的基本结构
AQS包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此AQS提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
AQS将节点加入到同步队列的过程如图:
节点加入到同步队列
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程如图:
首节点的设置
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

4. 独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法源码如下,注意看注释:

 public final void acquire(int arg) {
 		//当前是否获取到同步状态,如果没有这将CAS自旋将当前节点设置成尾节点,并自旋获取同步状态,如果获取成功则退出自旋,并唤醒当前线程。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//自旋获取同步状态,直到被前置节点唤醒或则被中断
final boolean acquireQueued(final Node node, int arg) {
       boolean failed = true;
       try {
           boolean interrupted = false;
           for (;;) {
               final Node p = node.predecessor();
               //如果前置节点是头节点,且自身获取到同步状态,则将自己设置成头节点,并退出阻塞,线程继续执行
               if (p == head && tryAcquire(arg)) {
                   setHead(node);
                   p.next = null; // help GC
                   failed = false;
                   return interrupted;
               }
               //检查和更新未能获取的节点的状态,如果前置节点已释放,且当前线程被中断
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)//取消获取同步状态
               cancelAcquire(node);
       }
   }
  //中断当前线程
static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
/**
检查和更新未能获取同步状态的节点的状态。如果线程阻塞则返回true,
**/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//前置节点状态
        int ws = pred.waitStatus;
        // 如果前置节点已释放同步状态
        if (ws == Node.SIGNAL) return true;
        //如果前置节点被中断,设置前置节点的前置节点为当前节点的前置节点,
        //否者自旋设置前置节点状态为SIGNAL(-1)
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	  /*
             * 	等待状态为0或者-3时 需要将当前节点状态设为-1,表示前置节点释放后当前节点释放
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
//阻塞当前线程,确认是否被中断
private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
      return Thread.interrupted();
  }
//等待队列中的尾节点,被volatile 修饰,多线程可见
private transient volatile Node tail;
//生成同步节点,当前节点准备变为等待队列中尾节点
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 快速尝试在尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //自旋直到自己被设置成尾节点
    enq(node);
    return node;
}
//自旋直到自己被设置成尾节点
private Node enq(final Node node) {
        for (;;) {
        	//获取最新的尾结点
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:

  • 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态(上面demo中我们重写了该方法)
  • 如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)
  • 通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,
  • 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。
  • 如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

在addWaiter中:通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。试想一下:如果使用一个普通的LinkedList来维护节点之间的关系,那么当一个线程获取了同步状态,而其他多个线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发地被添加到LinkedList时,LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。
在**enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
节点进入AQS之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
acquireQueued(final Node node,int arg)**方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这样做的原因有两个:

  • 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
  • 维护同步队列的FIFO原则。

该方法中,节点自旋获取同步状态的行为如图:
节点自旋获取同步状态
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程:
独占式同步状态获取流程
前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用AQS的**release(int arg)**方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。代码如下:

public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
		unparkSuccessor(h);
		return true;
	}
	return false;
}

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用
LockSupport 线程间通信来唤醒处于等待状态的线程。

总结:在获取同步状态时,AQS维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,AQS调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

5. 共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如图:
共享式与独占式访问资源的对比
在图中,左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法源码如下:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以
释放同步状态,该方法源码如下:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。

6. 独占式超时获取同步状态

通过调用AQS的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。
在分析该方法的实现前,先介绍一下响应中断的同步状态获取过程。在Java 5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:
nanosTimeout-=now-lastTime,
其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时,该方法源码如下:

 private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //计算时间,当前时间now减去睡眠之前的时间lastTime得到已经睡眠
				//的时间delta,然后被原有超时时间nanosTimeout减去,得到了
				//还应该睡眠的时间
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超时非常短的场景下,同步器会进入无条件的快速自旋。
独占式超时获取同步态的流程如图:独占式超时获取同步状态的流程
从图可以看出,独占式超时获取同步状态doAcquireNanos(int arg,long nanosTimeout)和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos(int arg,long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。

7. 简单的Demo

为了加深影响我们设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock。

  • 首先,确定访问模式。TwinsLock能够在同一时刻支持多个线程的访问,这显然是共享式访问,因此,需要使用AQS提供的acquireShared(int args)方法等和Shared相关的方法,这就要求TwinsLock必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,这样才能保证AQS的共享式同步状态的获取与释放方法得以执行。
  • 其次,定义资源数。TwinsLock在同一时刻允许至多两个线程的同时访问,表明同步资源数为2,这样可以设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程只能被阻塞。在同步状态变更时,需要使用compareAndSet(int expect,int update)方法做原子性保障。
  • 最后,组合自定义同步器。
    前面提到,自定义同步组件通过组合自定义同步器来完成同步功能,一般情况下自定义同步器会被定义为自定义同步组件的内部类。
public class TwinsLock implements Lock {
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -6008501609713522064L;

        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero.");
            }
            setState(count);
        }

        public int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }

        public boolean tryReleaseShared(int returnCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }
    }

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

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return sync.releaseShared(1);
    }

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

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

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

在上述示例中,TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock()方法获取锁,随后调用unlock()方法释放锁,而同一时刻只能有两个线程同时获取到锁。
TwinsLock同时包含了一个自定义同步器Sync,而该同步器面向线程访问和同步状态控制。以共享式获取同步状态为例:同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正确设置,当**tryAcquireShared(int reduceCount)**方法返回值大于等于0时,当前线程才获取同步状态,对于上层的TwinsLock而言,则表示当前线程获得了锁。
AQS作为一个桥梁,连接线程访问以及同步状态控制等底层技术与不同并发组件(比如Lock、CountDownLatch等)的接口语义。
下面编写一个测试来验证TwinsLock是否能按照预期工作。在测试用例中,定义了工作者
线程Worker,该线程在执行过程中获取锁,当获取锁之后使当前线程睡眠1秒(并不释放锁),
随后打印当前线程名称,最后再次睡眠1秒并释放锁,测试用例如下所示:

public class TwinsLockTest {

    public static void main(String[] args) throws InterruptedException {
        final Lock lock = new TwinsLock();
        // 启动10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        lock.lock();
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }

                }
            }, "Thread-" + i).start();
        }
        for (int i = 0; i < 30; i++) {
            Thread.sleep(1000);
            System.out.println();
        }
    }
}

运行该测试用例,可以看到线程名称成对输出,也就是在同一时刻只有两个线程能够获
取到锁,这表明TwinsLock可以按照预期正确工作。

输出结果如下:
Thread-0
Thread-2


Thread-1
Thread-3


Thread-5
Thread-4


Thread-7
Thread-6


Thread-9
Thread-8

8. 总结

总结一下:

  • AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,
  • AQS既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件。
  • 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步
    器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交
    互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,
    它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同
    步器很好地隔离了使用者和实现者所需关注的领域。
    其实上面的就是刚开始的简介,现在看是不是理解深刻。。。

参考文献 《Java并发编程的艺术》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章