ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
其实ReentrantLock并没有我们想的那么神秘,在我们之前的博客已经尝试使用过ReentrantLock,相信大家也有所感悟。ReentrantLock的底层是根据AbstractQueuedSynchronizer实现的,对于AbstractQueuedSynchronizer,可以参考我的这篇博客:Java并发编程之AQS
在学习ReentrantLock之前,我们有必要先了解一下什么是公平锁,什么是非公平锁。
公平锁:尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁在Java中,我们常用的synchronized实际上就是非公平锁。而对于我们将要讲解的ReentrantLock,它默认情况下是非公平锁,但是可以设置为公平锁。
ReentrantLock其实是一种可重入锁,若还不知道什么是可重入锁,可以参考我的这篇博客Java并发编程之Lock
我们先上ReentrantLock的定义
public class ReentrantLock implements Lock, java.io.Serializable实现了Lock接口
ReentrantLock内部维护了三个内部类,Sync、NonfairSync、FairSync,其中Sync继承了AbstractQueuedSynchronizer抽象类,而NonfairSync、FairSync继承了Sync,下面进行详细分析。
先上定义
abstract static class Sync extends AbstractQueuedSynchronizer这其实也是一个继承了AbstractQueuedSynchronizer的抽象静态类
NonfairSync采用了非公平方式来获得锁
static final class NonfairSync extends Sync { // 版本号 private static final long serialVersionUID = 7316153563782823691L; // 获得锁 final void lock() { if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用 // 把当前线程设置独占了锁 setExclusiveOwnerThread(Thread.currentThread()); else // 锁已经被占用,或者set失败 // 以独占模式获取对象,忽略中断 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }FairSyn采用了公平方式来获得锁
static final class FairSync extends Sync { // 版本序列化 private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 以独占模式获取对象,忽略中断 acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ // 尝试公平获取锁 protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取状态 int c = getState(); if (c == 0) { // 状态为0 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功 // 设置当前线程独占 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据 // 下一个状态 int nextc = c + acquires; if (nextc < 0) // 超过了int的表示范围 throw new Error("Maximum lock count exceeded"); // 设置状态 setState(nextc); return true; } return false; } }当资源空闲时,它总是会先判断sync队列是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。
实际上这就是一个同步队列,对ReentrantLock的操作大多转化为对sync的操作
默认采用非公平策略
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }通过传递参数确定采用公平策略或者是非公平策略
ReentrantLock的操作大多转化为对Sync的操作,实际上就是对AQS的操作。由于我们之前详细讲解过AQS,这里不再赘述。
对于ReentrantLock的实现来说,其实非公平锁的性能远远优于公平锁,为什么呢?原因在于,非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这体现在下面两个方面。
线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。ReentrantLock与synchronized的功能与内存语义大致相同
参考:【JUC】JDK1.8源码分析之ReentrantLock(三) Java多线程(九)之ReentrantLock与Condition 从源码角度彻底理解ReentrantLock(重入锁)