ReentrantLock是JUC包下的可重入的独占锁,可以在多线程的情况下保证线程安全,通过调用lock()和unlock()方法来进行加锁和解锁操作,使用示例如下
class X {
private final ReentrantLock lock = new ReentrantLock();
//其他属性
public void method() {
//加锁
lock.lock();
try {
//业务逻辑
} finally {
//解锁
lock.unlock();
}
}
}
那么这个加锁和解锁的源码是怎样的呢?需要从lock()方法的源码开始查看具体是实现。ReentrantLock中只有一个属性,内部类Sync,ReentrantLock的加锁内部实际上都是通过Sync来实现,ReentrantLock的构造方法有两个。源码如下
// 1. 无参构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
// 2.有参构造函数
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
通过这两个构造方法可以得到一下信息,ReentrantLock可以实现公平锁FairSync和非公平锁NonfairSync,默认是实现非公平锁NonfairSync。分析源码分两种场景分析,一种是公平锁,一种是非公平锁,我们首先阅读公平锁的加锁源码。
1. 公平锁FairSync的加锁源码
公平锁的加锁lock()源码详情如下,源码的注释在代码中注明了,话不多少,源码如下
//加锁
final void lock() {
acquire(1);
}
acquire()的源码如下
public final void acquire(int arg) {
//tryAcquire(arg)加锁逻辑,返回boolean,true加锁成功 false表示加锁失败
//如果加锁成功,取反,后面的&&逻辑不触发
//如果加锁失败,取反,执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的相关逻辑,先执行addWaiter(Node.EXCLUSIVE)方法,Node.EXCLUSIVE=null。然后执行acquireQueued()方法
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//内部调用线程Thread的interrupt()方法
selfInterrupt();
}
tryAcquire方法的加锁的源码
//tryAcquire方法的加锁的源码
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state的值,该属性由volatile,可以保证线程可见性
int c = getState();
//如果state的值为0
if (c == 0) {
//hasQueuedPredecessors用来判断队列中是不是轮到当前线程加锁
//compareAndSetState():CAS操作,将AQS中的state属性的值由0改为1,0--->1
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
//设置exclusiveOwnerThread属性设置为当前线程,exclusiveOwnerThread用来标识哪个线程拿到了锁
setExclusiveOwnerThread(current);
//返回true
return true;
}
}
//state的值不为0的情况
//先校验拿到锁的线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
//如果是当前线程,state的值加1
int nextc = c + acquires;
//健壮性校验
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置state的值
setState(nextc);
//返回true
return true;
}
//否则返回false,表示加锁失败
return false;
}
hasQueuedPredecessors()的源码如下
/**
* 该方法是AQS中的方法
* public final boolean hasQueuedPredecessors() {
* 尾节点
* Node t = tail;
* 头节点
* Node h = head;
* Node s;
* h != t: 如果这个判断为false,表示 h == t,头节点等于尾节点,表明现在是一个空队列,如果为true,表示队列中存在Node
*
* ((s = h.next) == null || s.thread != Thread.currentThread()):
* 1. (s = h.next) == null:头节点赋值给s,s==null如果为true,表示头节点为空,反之头节点不为空,这种是考虑到了多线程并发下,修改值还未修改完的情况
* 2. 尾节点不为空,(s = h.next) == null为false,再校验尾节点线程是否是当前线程,如果是,返回false
* return h != t &&
* ((s = h.next) == null || s.thread != Thread.currentThread());
* }
* 方法返回false的情况:
* 1. 头节点等于尾节点,空队列的时候
* 2. 头节点存在下一个节点,并且下一个节点就是当前线程
*/
public final boolean hasQueuedPredecessors() {
//获取尾结点
Node t = tail;
//获取头节点
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
校验到如果还没轮到当前线程加锁或者加锁失败,就会将线程放入队列,放入队列通过addWaiter()方法来实现,其源码如下
//将Node加入队列中
private Node addWaiter(Node mode) {
//创建一个Node对象,该对象中的nextWaiter为null
Node node = new Node(Thread.currentThread(), mode);
//获取队列中的尾节点
Node pred = tail;
//尾节点是否为空
if (pred != null) { //if里面的逻辑是一个入队列的处理逻辑,将新创建的节点添加到队列的尾部
//尾节点不为空
//将新建的node对象的上一节点指向尾节点
node.prev = pred;
//CAS操作,将尾节点由原值pred修改为node节点
if (compareAndSetTail(pred, node)) { //如果这里的CAS操作失败,就是compareAndSetTail返回false,那么线程将进入enq()方法中
//将原来的尾节点的next节点指向新创建的node节点
pred.next = node;
//返回新创建的节点node
return node;
}
}
//enq方法
enq(node);
//返回创建的node对象
return node;
}
//Node加入队列中
private Node enq(final Node node) {
//等价于while(true)操作,如果不触发中断或者跳出循环的话,会一直循环调用,起到一个线程阻塞的效果==>自旋锁
for (;;) {
//获取尾节点
Node t = tail;
if (t == null) {
//尾节点如果为空,创建一个空的Node对象,设置为头节点
//这里一开始有点难以理解
//我的理解是:AQS是一个阻塞队列,符合FIFO的顺序,如果尾节点不存在。说明还没有任务节点入队列。队列中是空的,这里创建的空Node节点,即使尾节点,也是头节点
if (compareAndSetHead(new Node()))
//将头节点赋值为尾节点,这里并没有跳出循环,那么执行完后
//队列中存在一个空的Node对象,然后执行下一次循环,下一次循环的话,尾节点就不在进入这里而是执行else的逻辑
tail = head;
} else {
//这里的逻辑和addWaiter()方法中if (pred != null)里面的逻辑一模一样
//就是将新node放入队列中
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//执行成功后,返回尾节点
return t;
}
}
}
}
从源码上来看,该方法通过自旋的方案,可以保证线程一定可以进入队列。执行完addWorker方法后,执行acquireQueued的方法,该方法的源码如下
//Node:刚加入队列中的Node,arg=1
//该方法用来加锁或者入队列并修改waitStatus的值
final boolean acquireQueued(final Node node, int arg) {
//失败flag
boolean failed = true;
try {
//线程中断flag
boolean interrupted = false;
//自旋了
for (;;) {
//获取Node的上一个节点prev
final Node p = node.predecessor();
//如果上一个节点是头节点,那么就去获取锁tryAcquire
if (p == head && tryAcquire(arg)) {
//如果获得锁成功,那么上一个节点需要出队列,出队列后,当前节点就是头节点了,所以需要设置当前节点为头节点
setHead(node);
//next设置为null,便于GC回收
p.next = null;
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire(p, node):该方法用来查询node节点的有效的上一个节点
if (shouldParkAfterFailedAcquire(p, node) &&
//使用park线程阻塞当前线程
parkAndCheckInterrupt())
//线程中断标记改为true
interrupted = true;
}
} finally {
if (failed)
//清除队列中失效或者不需要唤醒的Node节点
cancelAcquire(node);
}
}
//该方法用来查询node节点的有效的上一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//Node.SIGNAL:值为-1,为该状态时直接返回true
return true;
if (ws > 0) {
//循环
//node节点循环查询并链接上一个状态值为-1的节点
//找到后,将node节点的prev指向该节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//上一个节点的next连接到node
pred.next = node;
} else {
//将node的上一个节点状态改为Node.SIGNAL,-1。CAS操作
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
到这里,公平锁的加锁逻辑就看完了,进行加锁逻辑流程总结
- 调用ReentrantLock的lock()方法
- ReentrantLock的lock()方法会调用FairSync的lock进行加锁
- FairSync的lock()方法会调用acquire(1)方法
- acquire(1)调用tryAcquire(arg)加锁
- tryAcquire(arg)方法中通过hasQueuedPredecessors()方法进行校验,只有当队列为空或者已经排队到当前队列加锁的情况下,线程才可以进行加锁操作,这也是公平锁的公平所在,或者进行重入操作
- 如果tryAcquire(arg)方法判断当前线程还不能加锁,那么通过addWaiter方法将线程加入阻塞队列中进行排队
- 线程进入队列后,再通过shouldParkAfterFailedAcquire()方法将前面的节点等待状态改为-1,并阻塞当前线程
以上是公平锁的完整流程源码分析。接下来进行非公平锁的源码分析
2. 非公平锁NonfairSync的加锁源码
由于前面较为详细的介绍了公平锁的加锁源码,非公平锁的加锁原理上大同小异,这里我们只对差异的地方源码进行分析,第一个差异是在lock方法中,公平锁的lock方法中直接调用acquire()方法,但是非公平锁的实现原理不是的,其实现源码如下
final void lock() {
//先加锁操作【将state的值由0改为1】,如果加锁成功,将锁的线程修改为当前线程
//如果修改失败,执行acquire(1)的逻辑
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
从源码上可以看到,调用加锁的lock方法后直接先进行加锁操作。加锁失败的话,才调用acquire()方法。调用acquire()方法中在调用tryAcquire()方法。tryAcquire()方法在公平锁和非公平锁的实现上也存在不一样的逻辑,非公平锁的实现源码如下
protected final boolean tryAcquire(int acquires) {
//执行nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取锁的状态
int c = getState();
//如果state=0,表示锁未被其他线程占用,执行加锁逻辑
if (c == 0) {
//加锁操作,CAS操作,这里和公平锁的区别在于,公平锁会判断一下,是不是轮到当前线程加锁了。而非公平是不进行校验,直接进行加锁操作,修改state的值,其他逻辑
//和公平锁是一样的
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里并不会进行是否轮到该线程加锁了,而是直接加锁或者重入操作。以上lock()和tryAcquire()两个方法就是公平锁和非公平锁的差异所在,其他地方的处理逻辑,例如入队列等一系列逻辑都是一样的。这些源码阅读的话,还需要有一个知识点,Unsafe类,源码中出现了很多的compareAndSetxxx的方法,这类方法就是通过CAS实现来保证原子操作的,这类方法的源码可以顺便也看看,源码如下
//得到Unsafe类
private static final Unsafe unsafe = Unsafe.getUnsafe();
//操作state
private static final long stateOffset;
//操作头节点
private static final long headOffset;
//操作尾节点
private static final long tailOffset;
//操作Node节点的等待状态waitStatus
private static final long waitStatusOffset;
//操作Node节点的next
private static final long nextOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
3. ReentrantLock的解锁源码
有加锁,肯定就有解锁,那么ReentrantLock的解锁是如何做的呢?其实相对来说更简单一些了,解锁的逻辑核心就是将state的值修改为0的过程,每次unlock()实际就是一次state的是减1。源码如下
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//获取state的值,并减1
int c = getState() - releases;
//获取当前线程是否是拿到锁的线程,如果不是,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//标记,用来标记是否完全解锁,true表示解锁成功,false表示解锁未完成,存在重入操作
boolean free = false;
//state=0表示解锁成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
到此ReentrantLock的源码解读已经结束了。我们简单的进行一下总结,我个人的总结是如下几点
- ReentrantLock的加锁过程,实际上是对state的值修改来实现
- 在多线程场景下,使用CAS来实现state值修改的原子性,确保每次只能有一个线程修改成功
- 在多线程场景下,由于state是一个需要所有线程都可见的变量,所以必须确保线程间的可见性,所以state属性通过volatile修饰来达到线程间的可见性
- 另外,对于没有获得锁的线程,进入队列中进行等待再次加锁
以上是我根据阅读ReentrantLock的源码得到的一些总结。