ReentrantLock 重入锁,从这里开始解析Lock的源码及机制
首先从一个demo开始,这段代码循环5次,每次起一个线程,获取锁,执行逻辑,解锁.这篇的重点不在这个demo,无需过度关注.
public class ThreadMain {
private static int sum=0;
private static ReentrantLock lock=new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<5;i++){
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
if (finalI%2==0){
try {
System.out.println("--------"+finalI+"--------");
Thread.sleep(2000);
System.out.println("--------"+finalI+"--------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sum++;
System.out.println(sum+"-----"+ Thread.currentThread().getName());
lock.unlock();
}
}).start();
}
}
}
先从代码结构看起,ReentrantLock 类存在三个内部类分别是
Sync,FairSync,NonfairSync
其中FairSync,NonfairSync继承了Sync,而这个Sync继承了AbstractQueuedSynchronizer

AbstractQueuedSynchronizer的结构如下,AbstractQueuedSynchronizer中存在一个内部类Node,这个node就是本篇的重点了.

接下来重点分析 AbstractQueuedSynchronizer与Node.
Node结构如下,可以看到Node存在prev(前一个Node),next(后一个Node),thread(记录线程),nextWaiter(等待属性,后文会继续讲)等属性,从这个结构可以看出来,Node为双向链表,每个Node都会记录它的前一个节点与后一个节点,每个节点都会有thread属性,用来记录当前线程.

然后是AbstractQueuedSynchronizer,可以看到里面存在一个head(头节点)与一个tail(尾节点).

至此,可以放出一个结论 (过程分析下部分继续说明),重入锁(ReentrantLock)通过双向链表(Node)的方式使得线程序列化访问临界资源.如下图所示,node的双向链表,每个node记录前驱及后续节点,AbstractQueuedSynchronizer中的head记录头节点,tail记录尾节点.

接下来分析流程
打上断点,跟进,此时走的是第一个线程"Thread-0"


进入断点看到源码,由于测试main方法中定义的是公平锁,所以进入FairSync中的lock()方法

这里插一句,公平锁与非公平锁的区别就在于新的线程获取锁的时候,公平锁会排队到队尾,非公平锁会先尝试获取锁,没有获取到再排到队尾,非公平锁的源码如下,标记部分即为区别,这里可以看到进行了CAS操作,如果成功了,则将当前线程设置为锁的独占线程.

回到公平锁的源码 ,最后会执行acquire(1)这行代码.

acquire(1)里面的逻辑如下

这里稍微说道说道
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//尝试获取锁,获取成功返回false,失败则返回true
!tryAcquire(arg)
//添加一个新节点Node,并将其置于链表尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
//给当前线程设置一个中断标识位,线程并不会中断,只是一个标识位
selfInterrupt();
总结起来就是:试图获取锁,如果获取失败则添加一个Node对象置于链表尾部,并给当前线程设置一个中断标识位,如果锁获取成功,则不执行后续逻辑
当第一个线程进入时,此时锁并未被占有,所以能够获取成功

同样来分析一下这段代码
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//拿到当前锁的状态值
int c = getState();
if (c == 0) {
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");
//更新锁的状态值(重入锁,每次线程进入+1,释放则-1)
setState(nextc);
return true;
}
return false;
}
分析一下: 判断当前锁的状态值state,如果等于0,则意味着当前没有线程占有该锁,接下来执行以下逻辑
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
//判断当前的Node链表头(head)与链表尾(tail)是否不等,如果不等则判断链表头的下一个Node是否为空或者链表头的下一个Node记录的线程是否等于当前线程,以此来校验结构是否正确
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
//通过CAS更新State的值
compareAndSetState(0, acquires)
//将当前线程设置为锁的独占线程
setExclusiveOwnerThread(current)
将当前线程设置为锁的当前独占线程后,意味着锁获取成功,至此,第一个线程获取了锁,目前没有释放,下一个线程去获取锁时,必定无法获取成功,接下来分析下一个线程获取锁时发生了什么.
当下一个线程进入时,同样走入了tryAcquire(arg)这行代码,但必定无法获取成功,此时线程0正在持有锁,并且没有释放,那么线程1只能在后续排队,接下来分析,这个队是怎么排的.

接下来分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这行代码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//添加一个新节点,并将其放入链表中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
由此先看addWaiter(Node.EXCLUSIVE), arg)这行代码
添加一个独占节点,具体逻辑,进入断点查看

这里添加了一个节点,并将当前线程赋值给该Node的thread属性,之后判断当前链表的尾节点 ,
private Node addWaiter(Node mode) {
//获取当前线程,并将其赋值给Node
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点
Node pred = tail;
if (pred != null) {
//如果尾节点不为空,则意味着当前队列中还有其他线程,那么把node对象的前置节点赋值为当前的尾节点
node.prev = pred;
//通过CAS将当前节点修改为tail(尾节点标识)
if (compareAndSetTail(pred, node)) {
//修改完成后将之前尾节点的next指向当前的node
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
//获取尾节点
Node t = tail;
if (t == null) { // Must initialize
//如果尾节点为空,则通过CAS将头节点设置为一个空的Node
if (compareAndSetHead(new Node()))
//并把头节点赋值给尾节点
tail = head;
} else {
//如果尾节点不为空,则将node的前置节点赋值为当前的tail(尾节点)
node.prev = t;
//通过CAS赋值tail(尾节点)为当前的node
if (compareAndSetTail(t, node)) {
//将之前尾节点的next指向当前的node
t.next = node;
return t;
}
}
}
}
这里通过一个自旋来完成节点结构的维护,第一次进入时通过CAS给head(头节点)赋值一个空的Node,并将这个Node赋值给tail(尾节点),下一次进入时t就不为空了,进入else里面的逻辑,将node的前置赋值为t(尾节点),并将t的next赋值为node,这样就完成了一次双向链表的插入操作,将node插入到了当前双向链表的尾部,并将tail指向它.

接下来继续分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg)中acquireQueued(final Node node, int arg)这部分的代码.

这里也存在一个自旋,该方法的作用是维护队列中的线程包括更新waitStatus状态.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取节点的前置节点 (prev)
final Node p = node.predecessor();
//如果前一个节点是head,那么尝试获取一次锁
if (p == head && tryAcquire(arg)) {
//获取锁成功则将node设置为head(在这可以知道head节点即为获取了当前锁的节点)
setHead(node);
p.next = null; // help GC
failed = false;
//结束自旋
return interrupted;
}
//更新waitStatus状态,删除已经失效的线程对应的node,完成更新后阻塞该线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//通过多次循环来维护waitStatus的值
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//waitStatus为-1 代表着这个node的下一个节点(next)对应的线程处于可以唤醒状态
return true;
if (ws > 0) {
//waitStatus>0 代表着node的下一个节点对应的线程为失效状态,删除它
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//通过CAS更新waitStatus的值
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
正如上文代码注释所示,通过自旋维护waitStatus值之后,下一次再进入时会执行到如图所示代码,这行代码的作用即为阻塞当前线程

在当前线程被唤醒之前,无法执行到断点的下一行代码,线程阻塞

至此ReentrantLock的加锁过程执行完毕,下一步看看解锁时发生了什么.
跟入断点,进入unlock()源码,可以看到调用了release()方法

release()源码如下

接下来分析一下逻辑
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
//获取当前的head节点
Node h = head;
//head节点不为空,并且head节点的waitStatus不为0
if (h != null && h.waitStatus != 0)
//唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
//尝试释放锁的方法
protected final boolean tryRelease(int releases) {
//计算更新state值
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//将当前锁的独占线程置为空
setExclusiveOwnerThread(null);
}
//更新state值
setState(c);
return free;
}
//唤醒node对应的下一个节点
private void unparkSuccessor(Node node) {
//拿到node的waitStatus值,用于判断该node的下一个节点是否为可唤醒的
int ws = node.waitStatus;
if (ws < 0)
//如果状态<0则通过CAS修改waitStatus的值
compareAndSetWaitStatus(node, ws, 0);
//获取node的下一个节点
Node s = node.next;
//这里需要注意,如果node的下一个节点的waitStatus >0意味着线程为失效的,此时唤醒的是尾节点的前一个(可被唤醒的)节点对应的线程
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒s.thread 也就是头节点的下一个节点对应的线程
LockSupport.unpark(s.thread);
}
最后通过
LockSupport.unpark(s.thread);
这行代码将下一个节点唤醒.
唤醒后看看现象,此时线程1从阻塞中被唤醒

当线程1被唤醒后,进入下一次的循环,node的前一个节点为head节点,然后尝试去获取锁

于是回到了本文开始讲到的获取锁的流程

这里说一个特殊点,当释放锁时,会去唤醒当前节点的下一个节点对应的线程,如果那个线程是失效的,则会唤醒尾节点的前一个(可唤醒的)线程

也就意味着,被唤醒的这个线程,其实是"插队"的,它本应该是倒数第二个节点,执行顺序应该排在末位,但被提前唤醒了.当这个"插队线程"释放锁时,会重新唤醒head节点的下一个节点.

总结一下:
1:ReentrantLock通过AbstractQueuedSynchronizer的一个内部类Node来维护一个双向链表结构,每个node中记录了上一个节点和下一个节点以及waitStatus
2:公平锁获取锁时,会将自己的node置于链表队尾,非公平锁在获取锁时会直接尝试CAS操作(尝试插队),操作失败才会去排队(置于链表队尾)
3:node对象中的线程能否被唤醒,取决于该节点的上一个节点的waitStatus
4:waitStatus的变化状态为0 =>-1 =>0

本文详细分析了ReentrantLock的源码,包括内部类FairSync和NonfairSync,以及它们如何继承Sync和AbstractQueuedSynchronizer。通过双向链表Node实现线程的序列化访问,公平锁和非公平锁的区别在于获取锁的策略。在获取锁失败时,线程会被添加到链表尾部并进入等待状态,当锁被释放时,会唤醒下一个节点的线程。
275

被折叠的 条评论
为什么被折叠?



