转载-深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)

好文,写的比我好。欢迎去出处鉴赏:node

http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer安全

前言

Java中的FutureTask做为可异步执行任务并可获取执行结果而被你们所熟知。一般可使用future.get()来获取线程的执行结果,在线程执行结束以前,get方法会一直阻塞状态,直到call()返回,其优势是使用线程异步执行任务的状况下还能够获取到线程的执行结果,可是FutureTask的以上功能倒是依靠经过一个叫AbstractQueuedSynchronizer的类来实现,至少在JDK 1.五、JDK1.6版本是这样的(从1.7开始FutureTask已经被其做者Doug Lea修改成再也不依赖AbstractQueuedSynchronizer实现了,这是JDK1.7的变化之一)。可是AbstractQueuedSynchronizer在JDK1.8中还有以下图所示的众多子类:架构

这些JDK中的工具类或多或少都被你们用过不止一次,好比ReentrantLock,咱们知道ReentrantLock的功能是实现代码段的并发访问控制,也就是一般意义上所说的锁,在没有看到AbstractQueuedSynchronizer前,可能会觉得它的实现是经过相似于synchronized,经过对对象加锁来实现的。但事实上它仅仅是一个工具类!没有使用更“高级”的机器指令,不是关键字,也不依靠JDK编译时的特殊处理,仅仅做为一个普普统统的类就完成了代码块的并发访问控制,这就更让人疑问它怎么实现的代码块的并发访问控制的了。那就让咱们一块儿来仔细看下Doug Lea怎么去实现的这个锁。为了方便,本文中使用AQS代替AbstractQueuedSynchronizer。并发

细说AQS

相关厂商内容异步

关于红包、SSD云盘等核心技术集锦!

跟技术大牛,侃侃容器那些事儿!

中国技术开放日上海站:FinTech-技术重定金融,将来大有不一样(免费报名)

极光数据服务,教你如何洞察企业数字DNA

58集团架构与大数据应用创新专场

在深刻分析AQS以前,我想先从AQS的功能上说明下AQS,站在使用者的角度,AQS的功能能够分为两类:独占功能和共享功能,它的全部子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即使是它最有名的子类ReentrantReadWriteLock,也是经过两个内部类:读锁和写锁,分别实现的两套API来实现的,为何这么作,后面咱们再分析,到目前为止,咱们只须要明白AQS在功能上有独占控制和共享控制两种功能便可。工具

独占锁

在真正对解读AQS以前,我想先从使用了它独占控制功能的子类ReentrantLock提及,分析ReentrantLock的同时看一看AQS的实现,再推理出AQS独特的设计思路和实现方式。最后,再看其共享控制功能的实现。大数据

对于ReentrantLock,使用过的同窗应该都知道,一般是这么用它的:ui

reentrantLock.lock()
        //do something
        reentrantLock.unlock()

ReentrantLock会保证 do something在同一时间只有一个线程在执行这段代码,或者说,同一时刻只有一个线程的lock方法会返回。其他线程会被挂起,直到获取锁。从这里能够看出,其实ReentrantLock实现的就是一个独占锁的功能:有且只有一个线程获取到锁,其他线程所有挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒从新开始竞争锁。没错,ReentrantLock使用的就是AQS的独占API实现的。spa

那如今咱们就从ReentrantLock的实现开始一块儿看看重入锁是怎么实现的。线程

首先看lock方法:

如FutureTask(JDK1.6)同样,ReentrantLock内部有代理类完成具体操做,ReentrantLock只是封装了统一的一套API而已。值得注意的是,使用过ReentrantLock的同窗应该知道,ReentrantLock又分为公平锁和非公平锁,因此,ReentrantLock内部只有两个sync的实现:

公平锁:每一个线程抢占锁的顺序为前后调用lock方法的顺序依次获取锁,相似于排队吃饭。

非公平锁:每一个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的前后顺序无关,相似于堵车时,加塞的那些XXXX。

到这里,经过ReentrantLock的功能和锁的所谓排不排队的方式,咱们是否能够这么猜想ReentrantLock或者AQS的实现(如今不清楚谁去实现这些功能):有那么一个被volatile修饰的标志位叫作key,用来表示有没有线程拿走了锁,或者说,锁还存不存在,还须要一个线程安全的队列,维护一堆被挂起的线程,以致于当锁被归还时,能通知到这些被挂起的线程,能够来竞争获取锁了。

至于公平锁和非公平锁,惟一的区别是在获取锁的时候是直接去获取锁,仍是进入队列排队的问题了。为了验证咱们的猜测,咱们继续看一下ReentrantLock中公平锁的实现:

调用到了AQS的acquire方法:

从方法名字上看语义是,尝试获取锁,获取不到则建立一个waiter(当前线程)后放到队列中,这和咱们猜想的好像很相似。[G1]

先看下tryAcquire方法:

留空了,Doug Lea是想留给子类去实现(既然要给子类实现,应该用抽象方法,可是Doug Lea没有这么作,缘由是AQS有两种功能,面向两种使用场景,须要给子类定义的方法都是抽象方法了,会致使子类不管如何都须要实现另一种场景的抽象方法,显然,这对子类来讲是不友好的。)

看下FairSync的tryAcquire方法:

getState方法是AQS的方法,由于在AQS里面有个叫statede的标志位 :

事实上,这个state就是前面咱们猜测的那个“key”!

回到tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();  //获取父类AQS中的标志位
            if (c == 0) {
                if (!hasQueuedPredecessors() && 
                    //若是队列中没有其余线程  说明没有线程正在占有锁!
                    compareAndSetState(0, acquires)) { 
                    //修改一下状态位,注意:这里的acquires是在lock的时候传递来的,从上面的图中能够知道,这个值是写死的1
                    setExclusiveOwnerThread(current);
                    //若是经过CAS操做将状态为更新成功则表明当前线程获取锁,所以,将当前线程设置到AQS的一个变量中,说明这个线程拿走了锁。
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
             //若是不为0 意味着,锁已经被拿走了,可是,由于ReentrantLock是重入锁,
             //是能够重复lock,unlock的,只要成对出现行。一次。这里还要再判断一次 获取锁的线程是否是当前请求锁的线程。
                int nextc = c + acquires;//若是是的,累加在state字段上就能够了。
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

到此,若是若是获取锁,tryAcquire返回true,反之,返回false,回到AQS的acquire方法。

若是没有获取到锁,按照咱们的描述,应该将当前线程放到队列中去,只不过,在放以前,须要作些包装。

先看addWaiter方法:

用当前线程去构造一个Node对象,mode是一个表示Node类型的字段,仅仅表示这个节点是独占的,仍是共享的,或者说,AQS的这个队列中,哪些节点是独占的,哪些是共享的。

这里lock调用的是AQS独占的API,固然,能够写死是独占状态的节点。

建立好节点后,将节点加入到队列尾部,此处,在队列不为空的时候,先尝试经过cas方式修改尾节点为最新的节点,若是修改失败,意味着有并发,这个时候才会进入enq中死循环,“自旋”方式修改。

将线程的节点接入到队里中后,固然还须要作一件事:将当前线程挂起!这个事,由acquireQueued来作。

在解释acquireQueued以前,咱们须要先看下AQS中队列的内存结构,咱们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。

而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,表明有一个线程已经获取锁,事实上,队列的第一个节点就是表明持有锁的节点):

黄色节点为队列默认的头节点,每次有线程竞争失败,进入队列后其实都是插入到队列的尾节点(tail后面)后面。这个从enq方法能够看出来,上文中有提到enq方法为将节点插入队列的方法:

再回来看看

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)) {
             //若是当前的节点是head说明他是队列中第一个“有效的”节点,所以尝试获取,上文中有提到这个类是交给子类去扩展的。
                    setHead(node);//成功后,将上图中的黄色节点移除,Node1变成头节点。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && 
                //不然,检查前一个节点的状态为,看当前获取锁失败的线程是否须要挂起。
                    parkAndCheckInterrupt()) 
               //若是须要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。
                    interrupted = true;
            }
        } finally {
            if (failed) //若是有异常
                cancelAcquire(node);// 取消请求,对应到队列操做,就是将当前节点从队列中移除。
        }
    }

这块代码有几点须要说明:

1. Node节点中,除了存储当前线程,节点类型,队列中先后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为何须要这个状态呢?

缘由是:AQS的队列中,在有并发时,确定会存取必定数量的节点,每一个节点[G4] 表明了一个线程的状态,有的线程可能“等不及”获取锁了,须要放弃竞争,退出队列,有的线程在等待一些条件知足,知足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition一样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总须要一个变量来描述它,这个变量就叫waitStatus,它有四种状态:

分别表示:

  1. 节点取消
  2. 节点等待触发
  3. 节点等待条件
  4. 节点状态须要向后传播。

只有当前节点的前一个节点为SIGNAL时,才能当前节点才能被挂起。

2.  对线程的挂起及唤醒操做是经过使用UNSAFE类调用JNI方法实现的。固然,还提供了挂起指定时间后唤醒的API,在后面咱们会讲到。

到此为止,一个线程对于锁的一次竞争才告于段落,结果有两种,要么成功获取到锁(不用进入到AQS队列中),要么,获取失败,被挂起,等待下次唤醒后继续循环尝试获取锁,值得注意的是,AQS的队列为FIFO队列,因此,每次被CPU假唤醒,且当前线程不是出在头节点的位置,也是会被挂起的。AQS经过这样的方式,实现了竞争的排队策略。

看完了获取锁,在看看释放锁,具体看代码以前,咱们能够先继续猜下,释放操做须要作哪些事情:

  1. 由于获取锁的线程的节点,此时在AQS的头节点位置,因此,可能须要将头节点移除。
  2. 而应该是直接释放锁,而后找到AQS的头节点,通知它能够来竞争锁了。

是否是这样呢?咱们继续来看下,一样咱们用ReentrantLock的FairSync来讲明:

unlock方法调用了AQS的release方法,一样传入了参数1,和获取锁的相应对应,获取一个锁,标示为+1,释放一个锁,标志位-1。

一样,release为空方法,子类本身实现逻辑:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases; 
            if (Thread.currentThread() != getExclusiveOwnerThread()) //若是释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常。
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//由于是重入的关系,不是每次释放锁c都等于0,直到最后一次释放锁时,才通知AQS不须要再记录哪一个线程正在获取锁。
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

释放锁,成功后,找到AQS的头节点,并唤醒它便可:

值得注意的是,寻找的顺序是从队列尾部开始往前去找的最前面的一个waitStatus小于0的节点。

到此,ReentrantLock的lock和unlock方法已经基本解析完毕了,惟独还剩下一个非公平锁NonfairSync没说,其实,它和公平锁的惟一区别就是获取锁的方式不一样,一个是按先后顺序一次获取锁,一个是抢占式的获取锁,那ReentrantLock是怎么实现的呢?再看两段代码:

非公平锁的lock方法的处理方式是: 在lock的时候先直接cas修改一次state变量(尝试获取锁),成功就返回,不成功再排队,从而达到不排队直接抢占的目的。

而对于公平锁:则是老老实实的开始就走AQS的流程排队获取锁。若是前面有人调用过其lock方法,则排在队列中前面,也就更有机会更早的获取锁,从而达到“公平”的目的。

总结

这篇文章,咱们从ReentrantLock出发,完整的分析了AQS独占功能的API及内部实现,总的来讲,思路其实并不复杂,仍是使用的标志位+队列的方式,记录获取锁、竞争锁、释放锁等一系列锁的状态,或许用更准确一点的描述的话,应该是使用的标志位+队列的方式,记录锁、竞争、释放等一系列独占的状态,由于站在AQS的层面state能够表示锁,也能够表示其余状态,它并不关心它的子类把它变成一个什么工具类,而只是提供了一套维护一个独占状态。甚至,最准确的是AQS只是维护了一个状态,由于,别忘了,它还有一套共享状态的API,因此,AQS只是维护一个状态,一个控制各个线程什么时候能够访问的状态,它只对状态负责,而这个状态表示什么含义,由子类本身去定义。

感谢郭蕾对本文的审校。

相关文章
相关标签/搜索