ReentrantLock和condition源码浅析(一)
转载请注明出处。。。。。
一、介绍
大家都知道,在java中如果要对一段代码做线程安全操作,都用到了锁,当然锁的实现很多,用的比较多的是sysnchronize和reentrantlock,前者是java里的一个关键字,后者是一个java类。这两者的大致区别,在这里罗列下
相同点:
1、都能保证了线程安全性
2、都支持锁的重入
不同点:
1、synchronized适用于不是很激烈的情况,reentranlock适用于比较竞争激烈的情况
2、synchronized是jvm层面实现的锁机制,而reentranlock是java代码层面实现的锁机制。
3、reentranlock比synchronized多了锁投票,定时锁,中断锁等机制
4、synchronized是隐式获取锁和释放锁,不需要代码手动获取释放,reentranlock为显示获取锁和释放锁,必须要手动代码获取释放
要了解reentranlock,那肯定先得会用它,下面通过一个例子来了解它的加锁和释放锁过程
二、demo
1 public class demo { 2 3 private static int count = 0; 4 5 public static void main(string[] args) throws interruptedexception { 6 executorservice executorservice = executors.newfixedthreadpool(15); 7 for (int i = 0; i < 500; i++){ 8 executorservice.execute(() -> { 9 add(); 10 }); 11 } 12 thread.sleep(1000); 13 system.out.println(count); 14 } 15 16 private static int add(){ 17 return ++count; 18 } 19 }
上述代码,安装预期结果 那肯定是500,但是真的是500吗?结果如下
结果很显然,它是小于500的,把这段代码用锁保证结果和预期结果一致。代码如下
1 public class demo { 2 3 private static int count = 0; 4 5 private static lock lock = new reentrantlock(); 6 7 public static void main(string[] args) throws interruptedexception { 8 executorservice executorservice = executors.newfixedthreadpool(15); 9 for (int i = 0; i < 500; i++){ 10 executorservice.execute(() -> { 11 add(); 12 }); 13 } 14 thread.sleep(1000); 15 system.out.println(count); 16 } 17 18 private static int add(){ 19 lock.lock(); 20 try { 21 return ++count; 22 }finally { 23 lock.unlock(); 24 } 25 26 } 27 }
结果,和预期一致。
那它是怎么保证线程安全性的呢。往下看
三、reentrantlock分析
先来了解这个类的大致结构
红框圈中的三个类,其中sync是一个抽象类,另外两个是它的子类,sync又继承了aqs类,所以它也有锁的操作可能性。
fairsync是一个公平锁,nonfairsync是一个非公平锁,它们虽然继承了同一个类,但实现上有所不同,
1、非公平锁获取锁的过程
进入lock方法
而sync 是reentrantlock的一个字段,它在该类的构造函数中初始化,它有两个构造函数,sync默认为非公平锁实现,
当sync调用了lock方法,也就是调用nonfairsync类的lock方法,继续看下去,下图为该类的结构
lock大致步骤为,先去试着改变state的值,如果改变成功,则state值就变为1了,返回true,失败返回false,先来解释下compareandsetstate方法的作用
它有两个参数,第一个是期望值,第二个是要更新的值,如果内存中state值和期望值相等,则将内存值变为更新值,这是交换成功的标志。如果不相等,那肯定是false。这个方法其实就是cas,同时它也是线程安全的,具体实现,这里不作讨论。
这里也是获取锁成功的标志,当返回true,则将获取锁的线程置为当前线程,同时state值改变了,如果下一个线程进入,那么该方法肯定是返回false。那么获取锁失败的线程就会进入acquire方法。这个方法其实就是aqs的方法,代码如下,可以看到它又调用了tryacquire方法,而这个方法的实现就是上一个图的nonfairtryacquire方法,
1 final boolean nonfairtryacquire(int acquires) { 2 // 获取当前线程 3 final thread current = thread.currentthread(); 4 int c = getstate(); 5 // 如果状态值不为0,则进一步去获取锁 6 if (c == 0) { 7 if (compareandsetstate(0, acquires)) { 8 // 获取锁成功,将锁置给当前线程 9 setexclusiveownerthread(current); 10 return true; 11 } 12 }// 如果相等,则表明为锁的重入 13 else if (current == getexclusiveownerthread()) { 14 int nextc = c + acquires; 15 if (nextc < 0) // overflow 16 throw new error("maximum lock count exceeded"); 17 setstate(nextc); 18 return true; 19 } 20 // 只有获取锁失败才会返回false 21 return false; 22 }
当上面返回false时,又会相继执行addwaiter和acquirequeued方法,其中addwaiter方法主要是将获取锁失败的线程包装成一个node节点,插入一个队列中,注意头结点不是该节点,而是new了一个新的node节点
,它的状态值为0,然后返回该节点。
具体代码不做分析,下面看acquirequeued方法
1 final boolean acquirequeued(final node node, int arg) { 2 boolean failed = true; 3 try { 4 boolean interrupted = false; 5 // 类似while(true),作无限循环作用 6 for (;;) { 7 // 获取插入的node节点的前一个节点 8 final node p = node.predecessor(); 9 // 如果前继节点为head节点并且获取锁成功,则跳出无限循环,执行相应业务代码 10 if (p == head && tryacquire(arg)) { 11 sethead(node);// 头结点被改变,改变同时其状态也被改变了,节点线程也为空 12 p.next = null; // help gc 13 failed = false; 14 return interrupted; 15 } 16 // 前继节点不是头结点或获取锁失败 17 if (shouldparkafterfailedacquire(p, node) && 18 parkandcheckinterrupt()) 19 interrupted = true; 20 } 21 } finally { 22 // 防止代码运行过程中,线程突然被中断,中断则将该节点置为取消状态 23 if (failed) 24 cancelacquire(node); 25 } 26 }
其中shouldparkafterfailedacquire方法做了这两件事,
1、如果p节点前有状态为cancel的节点,则将这些取消的节点放弃掉,简单来说就是排除取消的节点
2、将p节点状态置为signal状态。等待下一次进入该方法可能会被挂起
方法parkandcheckinterrupt,在shouldparkafterfailedacquire返回true的时候,线程会被挂起。、
以上就是获取锁的过程,步骤如下
1、获取锁成功,则将改变state值,并将锁的拥有者置为当前线程
2、获取锁失败,则进入同步队列中,直到获取锁成功或当前线程被外因给中断,获取锁的过程中,有的线程可能会被挂起。
2、非公平锁释放锁的过程
为了不显得过于啰嗦,下面只列出核心代码
上述代码只有获取锁的线程调用了unlock方法,才会去修改state值,当state值为0时其他线程又可以获取锁,看到这,或许有的小伙伴迷糊了,上面不是介绍说在获取锁的过程中,有的线程会被挂起,
那如果挂起的线程node节点前继恰好是头结点,那岂不是运行不了?,莫慌,往下看。当state值置为0时,该方法会返回true,之后会执行下面方法。
重点方法在unparksuccessor方法上,看if(h != null && h.waitstatus !=0) ,为什么要加这个判断呢,因为如果有多个线程在获取锁,无论是获取失败,还是获取成功head节点的状态值都被改变(sethead()和shouldparkafterfailedacquire()方法会去改变head节点状态)。即不为0
如果为0,那么就说明就没有线程被挂起,自然就不用去释放这些线程。加这个判断,为了减少无用操作。重点来了,unparksuccessor方法,代码如下
private void unparksuccessor(node node) { // 将node结点状态置为0 int ws = node.waitstatus; if (ws < 0) compareandsetwaitstatus(node, ws, 0); /* * 如果node结点下一个节点为null或被取消则进入下面的for循环 * 下面的for循环从尾节点往前寻找没有取消的节点 ,直至最靠近node节点,即node节点下一个状态小于等于0的节点 * 在这里node节点就是头结点, */ node s = node.next; if (s == null || s.waitstatus > 0) { s = null; for (node t = tail; t != null && t != node; t = t.prev) if (t.waitstatus <= 0) s = t; } // 找到了该节点,释放该节点的线程 if (s != null) locksupport.unpark(s.thread); }
或许看到这更迷糊了,它释放锁怎么能确定释放的就是那个被挂起的线程呢,这个呢,确实确定不了,但是如果释放前继节点为头结点的线程,那么在后续获取锁的过程中,该线程肯定能获取到锁(因为这段代码是前一个线程释放锁的操作代码,所以下一个线程肯定能获取到锁),至此又一轮循环。
在这里,我对那个为啥从尾节点向前遍历也不清楚,如果有清楚的小伙伴,还请评论下方留言,谢谢!
以上就是非公平锁的释放操作。
3、公平锁的获取锁过程
该种锁和非公平锁的不同之处,就是这种锁一定得按照顺序来获取或,不能前一个线程释放了锁 ,然后谁抢到了就算谁的。
先来看下这种获取锁的代码
1 protected final boolean tryacquire(int acquires) { 2 final thread current = thread.currentthread(); 3 int c = getstate(); 4 if (c == 0) { 5 if (!hasqueuedpredecessors() && 6 compareandsetstate(0, acquires)) { 7 setexclusiveownerthread(current); 8 return true; 9 } 10 } 11 else if (current == getexclusiveownerthread()) { 12 int nextc = c + acquires; 13 if (nextc < 0) 14 throw new error("maximum lock count exceeded"); 15 setstate(nextc); 16 return true; 17 } 18 return false; 19 }
和非公平锁的不同点是在前者线程释放锁后(即state值为0),非公平锁是谁抢到锁,锁就是谁的,但是公平锁不一样,获取锁的线程会先去判断同步队列中有没有其他线程,如果没有,再去试着改变state值,如果改变成功则获取锁成功,它不允许没进入同步队列中的线程(此时同步队列中已有等待的线程,如果没有,那就是直接抢)抢占锁。下面看下hasqueuedprdecessor(),代码如下
1 public final boolean hasqueuedpredecessors() { 2 // the correctness of this depends on head being initialized 3 // before tail and on head.next being accurate if the current 4 // thread is first in queue. 5 node t = tail; // read fields in reverse initialization order 6 node h = head; 7 node s; 8 return h != t && 9 ((s = h.next) == null || s.thread != thread.currentthread()); 10 }
代码不复杂,就是判断同步队列中有没有等待的线程,且等待的线程不是当前线程,有则返回true,没有则返回false。
至于公平锁的释放操作,和非公平锁一致。这里不过多叙述。
获取公平锁操作
1、先判断同步队列中有没有等待的线程。
2、有则放弃锁的争夺,进入同步队列排好队,没有则抢占锁
----------------------------------------------------------------------------------------------------华丽的分界线---------------------------------------------------------------------------------------------------------------------------------
本来想继续写condition,但好像篇幅有点啰嗦,就放在下一篇。
以上就是我的个人见解,如果不足或错误之处,还请指教,谢谢!
上一篇: quartz的任务在tomcat下被调用两次的问题
下一篇: 偶遇真爱
推荐阅读
-
Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?
-
Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析
-
浅析Python 实现一个自动化翻译和替换的工具
-
Eureka源码探索(一)-客户端服务端的启动和负载均衡
-
浅析ebtables的概念和一些基本应用
-
vuex 源码分析(一) 使用方法和代码结构
-
Android ABC Jetpack学习之一文学会Navigation(附源码解析和使用封装)
-
死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁
-
结合Mybatis源码说说sqlSession创建流程和从中用到的一些设计模式
-
用canvas画飞机大战(一步步详解附带源代码,源码和素材上传到csdn,可以免费下载)