回答一个问题
在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized
关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?
我听过一句话非常的经典,也是我认为是每个人都应该了解的一句话:你以为的并不是你以为的
。明白什么意思么?不明白的话,加我微信我告诉你。
初识 ReentrantLock
ReentrantLock 位于 java.util.concurrent.locks
包下,它实现了 Lock
接口和 Serializable
接口。
ReentrantLock 是一把可重入锁
和互斥锁
,它具有与 synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,但是它比 synchronized 具有更多的方法和功能。
ReentrantLock 基本方法
构造方法
ReentrantLock 类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
第二个构造函数也是判断 ReentrantLock 是否是公平锁的条件,如果 fair 为 true,则会创建一个公平锁
的实现,也就是 new FairSync()
,如果 fair 为 false,则会创建一个 非公平锁
的实现,也就是 new NonfairSync()
,默认的情况下创建的是非公平锁
// 创建的是公平锁 private ReentrantLock lock = new ReentrantLock(true); // 创建的是非公平锁 private ReentrantLock lock = new ReentrantLock(false); // 默认创建非公平锁 private ReentrantLock lock = new ReentrantLock();
FairSync 和 NonfairSync 都是 ReentrantLock 的内部类,继承于 Sync
类,下面来看一下它们的继承结构,便于梳理。
abstract static class Sync extends AbstractQueuedSynchronizer {...} static final class FairSync extends Sync {...} static final class NonfairSync extends Sync {...}
在多线程尝试加锁时,如果是公平锁,那么锁获取的机会是相同的。否则,如果是非公平锁,那么 ReentrantLock 则不会保证每个锁的访问顺序。
下面是一个公平锁
的实现
public class MyFairLock extends Thread{ private ReentrantLock lock = new ReentrantLock(true); public void fairLock(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + "正在持有锁"); }finally { System.out.println(Thread.currentThread().getName() + "释放了锁"); lock.unlock(); } } public static void main(String[] args) { MyFairLock myFairLock = new MyFairLock(); Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "启动"); myFairLock.fairLock(); }; Thread[] thread = new Thread[10]; for(int i = 0;i < 10;i++){ thread[i] = new Thread(runnable); } for(int i = 0;i < 10;i++){ thread[i].start(); } } }
不信?不信你输出试试啊!懒得输出?就知道你懒得输出,所以直接告诉你结论吧,结论就是自己试
。
试完了吗?试完了我是不会让你休息的,过来再试一下非公平锁的测试和结论,知道怎么试吗?上面不是讲过要给 ReentrantLock 传递一个参数的吗?你想,传 true 的时候是公平锁,那么反过来不就是非公平锁了?其他代码还用改吗?不需要了啊。
明白了吧,再来测试一下非公平锁的流程,看看是不是你想要的结果。
公平锁的加锁(lock)流程详解
通常情况下,使用多线程访问公平锁的效率会非常低
(通常情况下会慢很多),但是 ReentrantLock 会保证每个线程都会公平的持有锁,线程饥饿的次数比较小
。锁的公平性并不能保证线程调度的公平性。
此时如果你想了解更多的话,那么我就从源码的角度跟你聊聊如何 ReentrantLock 是如何实现这两种锁的。
如上图所示,公平锁的加锁流程要比非公平锁的加锁流程简单,下面要聊一下具体的流程了,请小伙伴们备好板凳。
下面先看一张流程图,这张图是 acquire 方法的三条主要流程
首先是第一条路线,tryAcquire 方法,顾名思义尝试获取,也就是说可以成功获取锁,也可以获取锁失败。
使用 ctrl+左键
点进去是调用 AQS 的方法,但是 ReentrantLock 实现了 AQS 接口,所以调用的是 ReentrantLock 的 tryAcquire 方法;
首先会取得当前线程,然后去读取当前锁的同步状态,还记得锁的四种状态吗?分别是 无锁、偏向锁、轻量级锁和重量级锁
,如果你不是很明白的话,请参考博主这篇文章(不懂什么是锁?看看这篇你就明白了),如果判断同步状态是 0 的话,就证明是无锁的,参考下面这幅图( 1bit 表示的是是否偏向锁 )
如果是无锁(也就是没有加锁),说明是第一次上锁,首先会先判断一下队列中是否有比当前线程等待时间更长的线程(hasQueuedPredecessors);然后通过 CAS
方法原子性的更新锁的状态,CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值),expectedValue(期望更新的值),updateValue(更新的值)
,它们的更新如下
if(currentValue == expectedValue){ currentValue = updateValue }
CAS 通过 C 底层机制保证原子性,这个你不需要考虑它。如果既没有排队的线程而且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会获得偏向锁,记录获取锁的线程为当前线程。
然后我们看 else if
逻辑,如果读取的同步状态是1,说明已经线程获取到了锁,那么就先判断当前线程是不是获取锁的线程,如果是的话,记录一下获取锁的次数 + 1,也就是说,只有同步状态为 0 的时候是无锁状态。如果当前线程不是获取锁的线程,直接返回 false。
acquire 方法会先查看同步状态是否获取成功,如果成功则方法结束返回,也就是 !tryAcquire == false
,若失败则先调用 addWaiter 方法再调用 acquireQueued 方法
然后看一下第二条路线 addWaiter
这里首先把当前线程和 Node 的节点类型进行封装,Node 节点的类型有两种,EXCLUSIVE
和 SHARED
,前者为独占模式,后者为共享模式,具体的区别我们会在 AQS 源码讨论,这里读者只需要知道即可。
首先会进行 tail 节点的判断,有没有尾节点,其实没有头节点也就相当于没有尾节点,如果有尾节点,就会原子性的将当前节点插入同步队列中,再执行 enq 入队操作,入队操作相当于原子性的把节点插入队列中。
如果当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程。