ReentrantLock是一个常用的控制并发的工具类,其实现了同synchronized关键字一样的作用,同时提供了更多的方法,显得更加灵活,接下来通过ReentrantLock的两种模式的加锁和解锁方法具体分析源码实现。
ReentrantLock类实现了Lock接口,在本类中定义了一个继承AQS的抽象类Sync,用来控制共享资源的获取和释放。同时,定义了NonfairSync(非公平锁)和FairSync(公平锁)两个子类,这两个子类都继承Sync类,同时,它的无参构造方法默认创建非公平锁,而有参构造方法需要传入布尔类型的变量值控制创建公平锁或非公平锁。
在使用reentrantLock.lock()
方法时,无论我们创建的是公平锁模式还是非公平锁模式,lock方法调用的代码如下
public void lock() {
sync.lock();
}
可以看到,都是通过sync类调用lock实现方法,同理,unlock方法也是调用的同一块代码
public void unlock() {
sync.release(1);
}
在前文说过公平锁/非公平锁都是继承sync类,因此,sync.lock()
和sync.release(1)
分别有其不同的实现,下面分别阐述其实现方式
在NonfairSync类中定义了lock方法的实现,代码如下:
final void lock() {
//通过cas修改共享资源状态
if (compareAndSetState(0, 1))
//修改成功,将当前线程设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
//通过acquire方法获取共享资源
acquire(1);
}
可以看到,对共享资源的获取acquire
方法就是之前在AQS中介绍的,其源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们之前讲过,tryAcquire
方法在AQS中是一个模板方法,具体实现在ReentrantLock中
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
调用了nonfairTryAcquire
方法,即非公平模式尝试获取资源,而nonfairTryAcquire
是定义在Sync子类中的,由此可以看出Sync类是偏心的,它只定义了非公平模式的尝试获取锁,源码如下
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//共享资源状态值
int c = getState();
//c==0说明资源没被占用,即可以尝试获取
if (c == 0) {
//如果cas修改状态值成功
if (compareAndSetState(0, acquires)) {
//当前线程占锁成功,设置为独占线程
setExclusiveOwnerThread(current);
//上锁成功
return true;
}
}
//如果当前线程就是占锁线程,这里就是可重入锁的实现
else if (current == getExclusiveOwnerThread()) {
//新的状态值,即重入次数是之前的锁次数+当前获取次数
int nextc = c + acquires;
//注意这里是int类型,如果超过限制会抛出错误
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置新的锁次数,并返回上锁成功
setState(nextc);
return true;
}
//尝试上锁失败,那么会执行acquire的后续方法,即加入同步队列等一些列操作,可以参考前篇AQS相关内容
return false;
}
非公平锁的加锁方法就是这样,具体流程总结下来就是先通过cas尝试修改共享资源,修改成功就代表占锁成功;否则,调用acquire方法,先执行tryAquire方法尝试加锁,尝试加锁成功就完成操作(这里面包含重入操作),尝试加锁失败就进入AQS的一系列入队操作(CLH队列)和出队获取资源操作(acquireQueued(addWaiter(Node.EXCLUSIVE)
)
在FairSync类中定义的lock接口代码如下:
final void lock() {
acquire(1);
}
直接调用了acquire
方法,没有同非公平锁那样先通过cas操作占锁,这是和非公平锁的第一个区别。接下来看tryAcquire
方法的实现。这个方法是定义在FairSync类中的,代码如下
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取共享资源占用次数
int c = getState();
//如果共享资源未被占用
if (c == 0) {
//这里是和非公平锁的第二点不同,添加了hasQueuedPredecessors判断,只有hasQueuedPredecessors返回false才能继续通过cas操作更改共享资源状态值,hasQueuedPredecessors判断的是当前线程是否需要排队,剩下的操作同非公平锁一样
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//该方法主要判断当前线程需不需要排队,若需要排队,返回true,否则返回false
public final boolean hasQueuedPredecessors() {
//同步队列尾节点
Node t = tail;
//同步队列头节点
Node h = head;
//变量节点
Node s;
//结果一:h!=t
//返回false,即h==t,那么有两种情况,同步队列为空甚至没有初始化或者只有一个节点,这两种情况都是不需要排队的,因为同步队列中可以直接竞争锁的节点是头节点的下一个节点,返回false,这样就直接返回,无需排队
//返回true,看下面的结果二
//结果二:((s = h.next) == null || s.thread != Thread.currentThread())
//若(s = h.next) == null返回true,代表头节点的下一个节点是null,为什么同步队列有多个节点还会出现这种情况呢?只有一种情况,在执行enq方法时,if (compareAndSetHead(new Node()))
//tail = head;
//head节点更新为新建节点,next属性为null,并且tail=head还未执行,这有这样才能使得结果一返回true,那么此时已经有线程初始化同步队列了,当前线程肯定需要排队,返回true
//若(s = h.next) == null返回false,继续向后执行,s.thread != Thread.currentThread()若为true,说明首节点的下一个节点不是当前线程节点,需要排队,否则,当前线程是首节点的下一个节点线程,那么就不需要排队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
上面分别是非公平锁和公平锁的加锁方法的代码分析,接下来看一下解锁的相关代码。
前面说过,unlock
方法都是通过sync.release(1)
实现,不同于lock
的是release
方法是在AQS中定义的,因此不区分公平模式还是非公平模式,源码如下
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
同tryAcquire
方法类似,tryRelease
方法也是模板方法,具体实现还要看ReentrantLock类,该方法也是在Sync类中定义,源码如下
protected final boolean tryRelease(int releases) {
//释放后共享资源状态值
int c = getState() - releases;
//如果当前线程不是占锁线程
if (Thread.currentThread() != getExclusiveOwnerThread())
//抛出非法的监视器状态异常
throw new IllegalMonitorStateException();
//共享资源空闲状态
boolean free = false;
//如果共享资源状态值==0,即处于空闲状态
if (c == 0) {
//标位空闲状态
free = true;
//将独占线程设为null
setExclusiveOwnerThread(null);
}
//更新共享资源状态值
setState(c);
//返回资源空闲状态
return free;
}
可以看到释放锁的方法相对简单,总结一下就是如果共享资源完全释放,那么就会将同步队列的头节点的下一个节点唤醒去竞争资源。
通过上面的代码分析,可以看出公平锁与非公锁主要区别在于加锁时,非公平锁会先有一次cas操作,如果此时恰巧共享资源没有被占用,直接占锁返回,如果cas失败,和公平锁一样都进入tryAcquire
方法,而公平锁模式会判断线程是否需要排队,只有同步队列中可获取资源的头节点(注意这里指的是同步队列头节点的下一个节点)才能获取锁,其它节点均需要到同步队列排队,而非公平锁模式任何线程都可以去尝试获取锁。
因此,,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在同步队列中的线程长期处于饥饿状态。
在前面阅读AQS的相关源码之后,再回过头来看ReentrantLock的源码,理解起来就相对简单了,上面总结了ReentrantLock锁的加锁和解锁方法,对于其他的方法,基本都是一两行代码实现,例如isLocked
方法、getHoldCount
方法等,这里就不再赘述。总体来说,ReentrantLock锁是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,而且还可以和Condition条件接口相配合实现线程间的通信,而通过ReentrantLock创建的Condition条件实际也是通过new ConditionObject()
方法在AQS中实现的。