主要针对lock()、unlock()、锁的可重入性进行源码分析
先来看一个测试代码
public class Test7878 {
public static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
Thread t1 = new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"线程抢到锁,执行业务逻辑");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
},"t1");
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"主线程启动线程1");
t1.start();
System.out.println(Thread.currentThread().getName()+"主线程先抢锁");
Thread.sleep(5000L);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"主线程执行完毕");
}
}
创建了一个异步线程t1。与主线程互相抢夺lock锁
先来看一下公平锁的实现
Lock()
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
接着看到tryAcquire底层实现
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");
setState(nextc);
return true;
}
return false;
}
断点跟过来
此时初始状态下c==0,因此进入第一个条件判断,首先是hasQueuedPredecessors()
debug跟进来
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors()作用:
返回true:当头节点不等于尾节点并且(1.头节点指向的下一个节点为空<或者>2.头节点的下一个元素节点不是为当前线程)
解释一下:这里的链表结构为双向链表结构,存放的是除了当前加锁的线程外,其余的等待线程。比较特殊的是当前持有锁的线程并不在链表中,而是单独用一个变量来记录,因此头节点一般是null的。
可以简单理解为,此时线程要不要排队呢?
所以第一次进来,h=null,t=null,h!=t直接不满足,后面都不需要判断,因此不需要排队,直接返回false。
所以在并发的情况下,多个线程,只有一个是不需要排队的,然后才会进入后面的cas。
这也就是与非公平锁的区别,非公平锁不管排队不排队,直接开始cas,谁能cas成功,谁执行代码。
此时代码来到CAS
当前就主线程,因此第一次肯定CAS成功。
Cas成功,那么就设置exclusiveOwnerThread为当前线程
然后返回true,执行主线程代码
此时代码断点打在return false这里,准备看t1异步线程执行失败的情况。
一按f9,代码随机跳到return false这里
试运行一下
看到,确实当前线程t1,已经抢锁失败,返回false了。
然后!tryAcquire(arg)也就返回true了,执行后面的代码
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
底层源码如下
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
进入自旋过程
此时双向链表结构如下
可以看到当前传入的Node为
所以说,node获取前一个节点也就是,null的头节点,因此进入这个代码
p == head && tryAcquire(arg)
p==head
该条件满足,在走一次公平锁cas的过程,此时c0,依然走c0这个里面逻辑,此时看到
h!=t,条件满足了,说明可能要排队,再看看后面。
(s = h.next) == null:看看头节点的next是不是还有指向节点?是否非空,看debug控制台就知道,肯定不是,所以第一个判断是false,第二个呢,s.thread也就是t1,t1就是当前线程,所以肯定也不满足不等于条件。所以这里最终返回false也就是不需要需要排队。
然后就进入了cas
此时cas肯定是成功的,因为主线程已经执行完成,释放锁了。
最后debug跟过来,返回true。
最后控制台输出
main主线程启动线程1
main主线程先抢锁
main主线程执行完毕
t1线程抢到锁,执行业务逻辑
所以,异步线程cas失败后执行流程就分析完了。
如果此时主线程一直不释放锁呢?
断点直接再次来到tryAcquire()的return false这里。
看到acquireQueued()方法
刚才tryAcquire()为true,cas成功,但这里主线程没有释放锁,所以cas就失败了这里。
所以代码走到下面去
先看到shouldParkAfterFailedAcquire()方法
由于此时ws=0,所以逻辑跟到else里面
这里也就是cas,把ws从0变成这个枚举值-1,那这里是为啥呢,主要是防止自旋的时候空转。怎么实现的,那么接下来,自旋再次执行,代码依然走到这个方法,此时
ws = -1作为标识,说明我刚刚已经执行过了,所以直接返回true,返回true之后。
代码往上一步方法看
刚刚返回shouldParkAfterFailedAcquire()方法false,这次返回true,所以此时走到parkAndCheckInterrupt()方法
这里面就是解决由于cpu空转而产生CPU飙高问题的的核心。
代码跟进来
此时直接用到了LockSupport.park(this)去。直接挂起当前线程,所以代码在这里就执行完了,如果主线程不通过LockSupport.unpark()方法唤醒,那么代码就暂时一直卡在这了。
此时,F9一按,跳掉断点,发现断点直接不会走下去。所以当前t1线程也就被挂起了,所以就不会再去自旋了。
那么解锁的原理是什么呢?
点到源码unlock
点到这个方法里面,当前线程为main主线程。
此时h非空满足,但是状态还是0,所以不满足,因此此时直接释放锁,t1抢到锁。t1也是同样逻辑。
那另一个唤醒逻辑咋执行的呢,我们先让t1cas多失败几次然后挂起。
直接断点打在
此时t1直接挂起
然后到释放锁
断点提前打在return Thread.interrupted()这里
此时代码就走到了
即当前线程就为主线程
先判断
W<0,因为刚刚已经cas改成-1了,目的就是防止cpu空转,所以这时候又要cas改成0,为了一会可以成功的unlock()
接着代码取到
s不为空,所以直接走到
到这里,也就是唤醒我们刚刚阻塞的t1线程,让他重新CAS。
此时直接F9,挑掉。
代码来到
就是刚刚说的提前打的断点,也就是在这被唤醒了。
此时重新cas,就成功了
此时返回false,lock执行结束,exclusiveOwnerThread也就是t1线程了。
所以此时,t1线程就获取到锁了。
ReentrantLock底层锁的可重入是如何实现的呢?
其实就是在加锁的时候tryAcquire()方法实现的
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");
setState(nextc);
return true;
}
return false;
}
前面分析到的是c==0
下面这个判断就是对锁的可重入性的实现
只要判断到当前线程如果等于当前持有锁的线程,然后把c+入参传入的acquires,也就是0+1=1,然后再把1设置到state里面去,保存锁的重入次数。