可重复锁ReentrantLock原理分析
2020-06-18 本文已影响0人
平凡人笔记
可重入锁ReentrantLock实现层面依赖
一、CAS(compareAndSet)
LockSupport
基本的方法
park
park使得当前线程放弃cpu 进入等待(waiting)状态 操作系统不会再对其进行调度
直到其他线程对它调用了unpark方法,其中unpark方法使得参数指定的线程恢复可运行状态
[1] part和Thread.yield()区别
-
yield 只是告诉操作系统可以让其他线程先运行,但是自己可以仍是运行态
-
park 方法则是放弃线程的运行资格,使得线程进入 WAITING 等待状态
[2] 响应中断
park 方法是响应中断的,当有中断发生时,park方法会返回,并且重新设置线程的中断状态
[3]2个变体
-
parkNanos:可以指定等待的最长时间,参数是相对于当前时间的纳秒数
-
parkUntil:可以指定最长等待的时间,参数是绝对时间,相对于纪元时的毫秒数
当等待超时,方法就会返回。同时还有一些其他的变体,可以指定一个对象,表示是由于该对象而进行等待,以便于调试,一般情况下传递的参数为 this
getBlocker
二、AQS
- 提供了一个state字段 被volatile修饰 保证内存可见性、顺序性
- AQS内部维护了一个等待队列,借助CAS方法实现无阻塞算法进行更新
三、ReentrantLock
Sync是抽象类
NonfairSync是 fair 为 false 时使用的类[默认]
FairSync 是 fair 为 true 时需要使用的类
lock实现
该方法被子类重写
如果没有被锁定,则使用CAS进行锁定;如果当前线程已经被锁定,则增加锁定次数。
如果 tryArquire方法返回false,则acquire方法会继续调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。
其中,addWaiter 会新建一个节点 Node,代表当前线程,然后加入内部的等待队列中。
在当如等待队列之后,调用 acquireQueued 来尝试获取锁,其代码为
是一个死循环,在每次循环中,首先检查当前节点是否为第一个等待的节点,
如果是且能获取到锁,就将当前节点从等待队列中移除并且返回,
否则通过parkAndCheckInterrupt方法最终调用 LockSupport.park而放弃CPU,
进入等待状态,在被唤醒之后检查是否发生了中断,记录中断标志。并且返回中断标志
如果能获得锁则立即获得,如若不能则加入等待队列。被唤醒之后检查自己是否为第一个等待的线程,如果是且能获得锁则返回,否则继续等待。如果在该过程中发生了中断, lock 会记录中断标志位,但是不会提前返回或者抛除异常
unlock实现
tryRelease 方法会修改线程状态并且释放锁, unparkSuccessor 方法会调用 LockSupport.unpark 将第一个等待的线程唤醒
公平锁和非公平锁
公平锁比非公平锁在源码实现上就多了一个检查:当没有其他等待时间更长的线程时,才能获取到锁
公平锁模型
初始化时, state=0,表示没有线程过来抢锁。这时候,A线程请求锁,占了锁,把state+1
线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后线程B请求锁,线程B无法获取锁,生成节点进行排队
初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁
可重入锁:就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁
仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除
非公平锁模型
当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续休眠了
为什么不默认是公平锁
保证公平整体性能会比较低,其原因不是因为检查慢,而是因为会让活跃线程无法得到锁,从而进入等待状态,引起了频繁的上下文切换,降低了整体的效率
ReentrantLock tryLock()方法使用的是非公平锁
和synchronized比较
-
ReentrantLock可以实现与 synchronized 相同的语义 而且支持以非阻塞方式获取锁,也可以想用中断,限时阻塞,更为灵活;synchronized 的使用更为简单,代码量也更少
-
synchronized 代表的是一种声明式编程思维 由 Java 系统负责实现 程序员并不清楚实现细节;显式锁代表一种命令式编程思维,使用者需要实现所有的细节
-
声明式编程的好处除了简单,在性能上也有所体现。在较新版本的 JVM 上,ReentrantLock和synchronized的性能是接近的, 并且 Java 编译器和虚拟机会不断优化 synchronized 的实现,比如自动分析 synchronized 的使用,对于没有锁竞争的场景,自动忽略对获取锁/释放锁的调用
-
能用 synchronized 就用 synchronized,不满足使用要求的时候考虑使用 ReentrantLock
代码资源
https://gitee.com/pingfanrenbiji/myconcurrent/blob/master/src/main/java/pers/hanchao/concurrent/reentrantLock/LockSupportTest.java
参考文章
https://blog.csdn.net/u011669700/article/details/80070892
https://blog.csdn.net/yanyan19880509/article/details/52345422