在JDK1.5以前,咱们在编写并发程序的时候无一例外都是使用synchronized来实现线程同步的,而synchronized在JDK1.5以前同步的开销较大效率较低,所以在JDK1.5以后,推出了代码层面的Lock接口(synchronized为jvm层面)来实现与synchronized一样功能的同步锁,而且针对不一样的并发场景也加入了许多个性锁功能。html
在java.util.concurrent.locks包中有不少Lock的实现类,经常使用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类简称AQS,他是实现Lock接口全部锁的核心。java
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 一、以获取锁的线程执行完同步代码,释放锁node 二、线程执行发生异常,jvm会让线程释放锁编程 |
在finally中必须释放锁,否则容易形成线程死锁 |
锁的获取 | 假设A线程得到锁,B线程等待。若是A线程阻塞,B线程会一直等待 | 分状况而定,Lock有多个锁获取的方式,具体下面会说道,大体就是能够尝试得到锁,线程能够不用一直等待 |
锁状态 | 没法判断 | 能够判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可中断 可公平/非公平(二者皆可) |
性能 | 少许同步 | 大量同步 |
对于synchronized来讲,其实没有什么所谓的源码去分析,synchronized是Java中的一个关键字,他的实现时基于jvm指令去实现的下面咱们写一个简单的synchronized示例并发
咱们点击查看SyncDemo.java的源码SyncDemo.class,能够看到以下:
框架
如上就是这段代码段字节码指令,没你想的那么难吧。言归正传,咱们能够清晰段看到,其实synchronized映射成字节码指令就是增长来两个指令:monitorenter和monitorexit。咱们看到上面的class文件第九、1三、19行,分别加入了monitorenter、monitorexit、monitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试得到锁,若是得到锁那么锁计数+1(为何会加一呢,由于它是一个可重入锁,因此须要用这个锁计数判断锁的状况),若是没有得到锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。jvm
那么有的朋友看到这里就疑惑了,那图上有2个monitorexit呀?立刻回答这个问题:上面我之前写的文章也有表述过,synchronized锁释放有两种机制,一种就是执行完释放;另一种就是发送异常,虚拟机释放。图中第二个monitorexit就是发生异常时执行的流程,这就是我开头说的“会有2个流程存在“。并且,从图中咱们也能够看到在第14行,有一个goto指令,也就是说若是正常运行结束会跳转到22行执行。源码分析
针对Lock接口中的锁,咱们来一个个来深刻分析,首先须要了解下面的名词概念性能
ReentrantLock至关因而对synchronized的一个实现,他与synchronized同样是一个可重入锁而且是一个独占锁,可是synchronized是一个非公平锁,任何处于竞争队列的线程都有可能获取锁,而ReentrantLock既能够为公平锁,又能够为非公平锁。ui
根据上面的源码咱们可知,ReentrantLock继承于Lock接口,而且是并发编程大师Doug Lea所创做(向大师致敬)而且在源码中咱们能够发现,不少操做都是基于Sync这个类实现的,而Sync是一个内部抽象静态类,继承AQS类。而Sync又有两个子类:
非公平锁子类:
static final class NonfairSync extends Sync
公平锁子类:
static final class FairSync extends Sync
他们两个的实现大体相同,差异不大,而且ReentrantLock的默认是采用非公平锁,非公平锁相对公平锁而言在吞吐量上有较大优点,咱们分析源码也主要从非公平锁入手。
本人主要经过ReentrantLock的加锁,到解锁的源码流程来分析。ReentrantLock的类图以下
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,立刻咱们会看到,这个volatile变量是全部Lock实现锁的关键。加锁、解锁的状态全都围绕这个状态位去实现。
闲话很少说,首先是ReentrantLock的非公平锁的加锁方法lock()
1.第一步,加锁lock()
公平锁的加锁方法lock()以下
咱们能够看到非公平锁与公平锁的加锁区别在于,非公平锁首先会进行一次CAS,去尝试修改AQS中的锁标记state字段,将其从0(无锁状态),修改成1(锁定状态)(注:ReentrantLock用state表示“持有锁的线程已经重复获取该锁的次数”。当state等于0时,表示当前没有线程持有锁),若是成功,就设置ExclusiveOwnerThread的值为当前线程(Exclusive是独占的意思,ReentrantLock用exclusiveOwnerThread表示“持有锁的线程”)
,若是成功,执行setExclusiveOwnerThread方法将持有锁的线程(ownerThread)设置为当前线程,不然就执行acquire方法,而公平锁线程不会尝试去获取锁,直接执行acquire方法。
2.acquire方法
根据acquire方法的注释大概能知道他的做用:
获取独占模式,忽略中断。经过调用至少一次tryAcquire方法,成功则返回。不然,线程可能排队。重复阻塞和解除阻塞,调用tryAcquire直到成功。
acquire方法的执行逻辑为,首先调用tryAcquire尝试获取锁,若是获取不到,则调用addWaiter方法将当前线程加入包装为Node对象加入队列队尾,以后调用acquireQueued方法不断的自旋获取锁。
其中tryAcquire方法、addWaiter方法、acquireQueued方法咱们接下来逐个分析。
3.tryAcquire方法
非公平锁中的tryAcquire方法直接调用Sync的nofairTryAcquire方法,源码以下:
nofairTryAcquire方法的逻辑:
咱们回到第2步的acquire方法,当tryAcquire方法返回true,说明没有线程占用锁,当前线程获取锁成功,后续的addWaiter方法与acquireQueued方法再也不执行并返回,线程执行同步块中的方法。若tryAcquire方法返回false,说明当前有其余线程占用锁,此时将会触发执行addWaiter方法与acquireQueued方法。
公平锁中的tryAcquire方法与非公平锁基本相同,只不过比非公平锁在第一次获取锁时的判断中多了hasQueuedPredecessors方法
hasQueuedPredecessors方法用于判断当前线程是否为head节点的后续节点线程(预备获取锁的线程节点)。
或者说:判断“当前线程”是否是CLH队列中的第一个线程线程(head节点的后置节点),如果的话返回false,不是返回true。
说明: 经过代码,能分析出,hasQueuedPredecessors() 是经过判断"当前线程"是否是在CLH队列的队首,来返回AQS中是否是有比“当前线程”等待更久的线程。下面对head、tail和Node进行说明。
4.addWaiter方法
addWaiter方法的主要目的是将当前线程包装为一个独占模式的Node隐式队列,在分析方法前咱们须要了解Node类的几个重要的参数:
prev:前置节点;
next:后置节点;
waitStatus:是等待链表(队列)中的状态,状态分一下几种
而在AQS中,记录了Node的头结点head,和尾节点tail
首先将当前线程保障成一个独占模式的Node节点对象,而后判断当前队尾(tail节点)是否有节点,若是有,则经过CAS将队尾节点设置为当前节点,并将当前节点的前置节点设置为上一个尾节点。
若是tail尾节点为null,说明当前节点为第一个入队节点,或者CAS设置当前节点为尾节点失败,将调用enq方法。
enq方法也是一个CAS方法,当第一次循环tail尾节点为null时,说明当前节点为第一个入队节点,此时将新建一个空Node节点为傀儡节点,并将其设置为队首,而后再次循环时,将当前节点设置为tail尾节点,失败将循环设定直至成功。如果addWaiter方法中设置tail尾节点失败的话,进入enq方法后直接将进入else模块将当前节点设置为tail尾节点,循环设定直至成功。
接了方便理解addWaiter方法的做用,以及后续acquireQueued的理解,咱们经过3个线程来画图演示从第1步到第4步AQS中Node队列的状况:
1.假设当前有一个线程 thread-1执行lock方法,因为此时没有其余线程占用锁,thread-1获得了锁
2.此时thread-2执行lock方法,因为此时thread-1占用锁,所以thread-2执行acquire方法,而且thread-1不释放锁,tryAcquire方法失败,执行addWaiter方法
因为thread-2第一个进入队列,此时AQS中head以及tail为null,所以进入执行enq方法,根据上面描述的enq方法逻辑,执行以后等待队列为
3.接下来thread-3执行lock方法,thread-1依然没有释放锁,此时对接就变成这样
addWaiter方法的的做用就是将一个个没有获取锁的线程,包装成为一个等待队列。
5.acquireQueued方法
acquireQueued方法的做用就是CAS循环获取锁的方法,而且若是当前节点为head节点的后续节点,则尝试获取锁,若是获取成功则将当前节点置为head节点,并返回,若是获取失败或者当前节点并非head节点的后续节点,则调用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法,将当前节点的前置节点状态位置为SIGNAL(-1) ,并阻塞当前节点。
6.shouldParkAfterFailedAcquire方法
shouldParkAfterFailedAcquire方法根据源码分许咱们能够得知,该方法就是用来设置前置节点的状态位为SIGNAL(-1),而SIGNAL(-1)表示节点的后置节点处于阻塞状态。首次进入该方法时,前置节点的waitStatus为0,所以进入else代码块中,经过CAS将waitStatus设置为-1,当外围方法acquireQueued再次循环时,将直接返回true。这时将知足判断条件执行parkAndCheckInterrupt方法。
而中间这块判断逻辑则是前置节点状态为CANCELLED(1),则继续查找前置节点的前驱节点,由于当head节点唤醒时,会跳过CANCELLED(1)节点(CANCELLED(1):由于超时或中断或异常,该线程已经被取消)。
摘取网上大神的shouldParkAfterFailedAcquire方法的逻辑总结:
若是前继的节点状态为SIGNAL,代表当前节点须要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将致使线程阻塞
若是前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,致使线程阻塞(parkAndCheckInterrupt)
若是前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与第2点相同
整体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,若是前继节点处于CANCELLED状态,则顺便删除这些节点从新构造队列。
7.parkAndCheckInterrupt方法
很简单,就是讲将前线程中断,返回中断状态。
那么此时咱们来经过画图,总结下步骤5~步骤7:
在thread-2与thread-3执行完步骤4后
此时thread-2执行步骤5,因为他的前置节点为Head节点所以它有了一次tryAcquire获取锁的机会,若是成功则设置thread-2的Node节点为head节点而后返回,因为当前节点没有被中断,所以返回的中断标记位为false。
若是tryAcquire获取锁依然失败,则调用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法对线程进行阻塞,直到持有锁的线程释放锁时被唤醒(具体后续说明,如今只须要知道前置节点获取释放锁后,会唤醒他的后置节点的线程),此时执行了shouldParkAfterFailedAcquire方法后将会变成这样
当锁被释放thread-2被唤醒后再次执行tryAcquire获取锁,此时因为锁已释放获取将会成功,但因为当前节点被中断过interrupted为true,所以返回的中断标记位为true。
回到上面步骤2中,此时将会执行selfInterrupt方法将当前线程从阻塞状态唤醒。
而thread-3则和thread-2经历差很少,区别在于thread-3的前置节点不是head节点,所以进入acquireQueued方法后thread-3直接被阻塞,直到thread-2获取锁后变为head节点而且释放锁以后,thread-3才会被唤醒。thread-3进入acquireQueued方法后变为
(为了不你们理解不了,此处再次说明,前置节点的waitStatus为-1时表示当前节点处于阻塞态)
下面来讲说步骤5中acquireQueued方法的finally代码块
cancelAcquire方法:若是出现异常或者出现中断,就会执行finally的取消线程的请求操做,核心代码是node.waitStatus = Node.CANCELLED;将线程的状态改成CANCELLED。
private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // 获取前置节点并判断状态,若是前置节点已被取消,则将其丢弃从新指向前置节点,直到指向一个距离当前节点最近的有效节点,这种处理很是巧妙让人佩服 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; //获取新的前置节点的后置节点(此时新的前置节点的next尚未指向当前节点) Node predNext = pred.next; //将当前节点设置为取消状态 node.waitStatus = Node.CANCELLED; // 若是当前节点为尾部节点,直接丢弃 if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { //若是前置节点不是head,则后置节点须要一个状态,来对标记当前节点的状态,此处是设置新的前置节点的waitStatus为SIGNAL(-1),而且将新的前置节点的next指向当前节点,当前节点不会再此处被丢弃,而是在shouldParkAfterFailedAcquire方法中丢弃 int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { //若是前置节点为head,则直接唤醒距离当前节点最近的有效后置节点 unparkSuccessor(node); } node.next = node; // help GC } }
unparkSuccessor方法源码以下:
首先,将当前节点waitStatus设置为0,而后获取距离当前节点最近的有效后置节点,最后unpark唤醒后置节点的线程,此时后置节点线程就有机会获取锁了
至此,全部的lock逻辑所有走完了,下面来讲说解锁。
ReentrantLock的unlock方法
实际上是调用的AQS的release方法
release方法的逻辑为,首先调用tryRelease方法,若是返回true,就执行unparkSuccessor方法唤醒后置节点线程。接下来咱们看看tryRelease方法,在ReentrantLock中实现。
咱们看到在tryRelease方法中首先会获取state锁标记,将其进行-1操做,而且返回结果根据state锁标记位是否为0,若是为0则返回true,不然返回false。
咱们知道ReentrantLock是一个可重入锁,前面分析了同一个线程,每次获取锁,重入锁,都会为state锁标记+1,state记录了线程获取了多少次锁。那么同一个线程获取了多少次锁,就要进行多少次解锁,直到所有解锁,state锁标记为0时,表示解锁成功,tryRelease方法返回true,后续唤醒后置节点线程。
至此ReentrantLock源码分析完毕,其余锁待续。。。
参考:
https://blog.csdn.net/wangxiaotongfan/article/details/51800981
https://www.cnblogs.com/sheeva/p/6472949.html
http://www.cnblogs.com/lcchuguo/p/5036172.html
https://www.jianshu.com/p/e4301229f59e
https://blog.csdn.net/mayongzhan_csdn/article/details/79374996
https://blog.csdn.net/Luxia_24/article/details/52403033
https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1