- 最重要的是我们新建一把
CLHLock
对象时,此时会执行构造函数里面的初始化逻辑。此时给尾指针tailNode
和当前节点curNode
初始化一个locked
状态为false
的CLHNode
节点,此时前继节点preNode
存储的是null
。
4.2 CLH锁的加锁过程
我们再来看看CLH锁的加锁过程,下面再贴一遍加锁lock
方法的代码:
// CLHLock.java
/**
- 获取锁
*/
public void lock() {
// 取出当前线程ThreadLocal存储的当前节点,初始化值总是一个新建的CLHNode,locked状态为false。
CLHNode currNode = curNode.get();
// 此时把lock状态置为true,表示一个有效状态,
// 即获取到了锁或正在等待锁的状态
currNode.locked = true;
// 当一个线程到来时,总是将尾结点取出来赋值给当前线程的前继节点;
// 然后再把当前线程的当前节点赋值给尾节点
// 【注意】在多线程并发情况下,这里通过AtomicReference类能防止并发问题
// 【注意】哪个线程先执行到这里就会先执行predNode.set(preNode);语句,因此构建了一条逻辑线程等待链
// 这条链避免了线程饥饿现象发生
CLHNode preNode = tailNode.getAndSet(currNode);
// 将刚获取的尾结点(前一线程的当前节点)付给当前线程的前继节点ThreadLocal
// 【思考】这句代码也可以去掉吗,如果去掉有影响吗?
predNode.set(preNode);
// 【1】若前继节点的locked状态为false,则表示获取到了锁,不用自旋等待;
// 【2】若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待
while (preNode.locked) {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
System.out.println(“线程” + Thread.currentThread().getName() + “没能获取到锁,进行自旋等待。。。”);
}
// 能执行到这里,说明当前线程获取到了锁
System.out.println(“线程” + Thread.currentThread().getName() + “获取到了锁!!!”);
}
虽然代码的注释已经很详细,我们还是缕一缕线程加锁的过程:
- 首先获得当前线程的当前节点
curNode
,这里每次获取的CLHNode
节点的locked
状态都为false
; - 然后将当前
CLHNode
节点的locked
状态赋值为true
,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态; - 因为尾指针
tailNode
的总是指向了前一个线程的CLHNode
节点,因此这里利用尾指针tailNode
取出前一个线程的CLHNode
节点,然后赋值给当前线程的前继节点predNode
,并且将尾指针重新指向最后一个节点即当前线程的当前CLHNode
节点,以便下一个线程到来时使用; - 根据前继节点(前一个线程)的
locked
状态判断,若locked
为false
,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待。
为了更通俗易懂,我们用一个图里来说明。
**假如有这么一个场景:**有四个并发线程同时启动执行lock操作,假如四个线程的实际执行顺序为:threadA<–threadB<–threadC<–threadD
第一步,线程A过来,执行了lock操作,获得了锁,此时locked
状态为true
,如下图:
第二步,线程B过来,执行了lock操作,由于线程A还未释放锁,此时自旋等待,locked
状态也为true
,如下图:
第三步,线程C过来,执行了lock操作,由于线程B处于自旋等待,此时线程C也自旋等待(因此CLH锁是公平锁),locked
状态也为true
,如下图:
第四步,线程D过来,执行了lock操作,由于线程C处于自旋等待,此时线程D也自旋等待,locked
状态也为true
,如下图:
这就是多个线程并发加锁的一个过程图解,当前线程只要判断前一线程的locked
状态如果是true
,那么则说明前一线程要么拿到了锁,要么也处于自旋等待状态,所以自己也要自旋等待。而尾指针tailNode
总是指向最后一个线程的CLHNode
节点。
4.3 CLH锁的释放锁过程
前面用图解结合代码说明了CLH锁的加锁过程,那么,CLH锁的释放锁的过程又是怎样的呢? 同样,我们先贴下释放锁的代码:
// CLHLock.java
/**
- 释放锁
*/
public void unLock() {
// 获取当前线程的当前节点
CLHNode node = curNode.get();
// 进行解锁操作
// 这里将locked至为false,此时执行了lock方法正在自旋等待的后继节点将会获取到锁
// 【注意】而不是所有正在自旋等待的线程去并发竞争锁
node.locked = false;
System.out.println(“线程” + Thread.currentThread().getName() + “释放了锁!!!”);
// 小伙伴们可以思考下,下面两句代码的作用是什么???
CLHNode newCurNode = new CLHNode();
curNode.set(newCurNode);
// 【优化】能提高GC效率和节省内存空间,请思考:这是为什么?
// curNode.set(predNode.get());
}
可以看到释放CLH锁的过程代码比加锁简单多了,下面同样缕一缕:
- 首先从当前线程的线程本地变量中获取出当前
CLHNode
节点,同时这个CLHNode
节点被后面一个线程的preNode
变量指向着; - 然后将
locked
状态置为false
即释放了锁;
注意:
locked
因为被volitile
关键字修饰,此时后面自旋等待的线程的局部变量preNode.locked
也为false
,因此后面自旋等待的线程结束while
循环即结束自旋等待,此时也获取到了锁。这一步骤也在异步进行着。
- 然后给当前线程的表示当前节点的线程本地变量重新赋值为一个新的
CLHNode
。
思考:这一步看上去是多余的,其实并不是。请思考下为什么这么做?我们后续会继续深入讲解。
我们还是用一个图来说说明CLH锁释放锁的场景,接着前面四个线程加锁的场景,假如这四个线程加锁后,线程A开始释放锁,此时线程B获取到锁,结束自旋等待,然后线程C和线程D仍然自旋等待,如下图:
以此类推,线程B释放锁的过程也跟上图类似,这里不再赘述。
4.4 考虑同个线程加锁释放锁再次正常获取锁的情况
在前面4.3小节讲到释放锁unLock
方法中有下面两句代码:
CLHNode newCurNode = new CLHNode();
curNode.set(newCurNode);
这两句代码的作用是什么?这里先直接说结果:若没有这两句代码,若同个线程加锁释放锁后,然后再次执行加锁操作,这个线程就会陷入自旋等待的状态。这是为啥,可能有些下伙伴也没明白,劲越也是搞了蛮久才搞明白,嘿嘿。
下面我们同样通过一步一图的形式来分析这两句代码的作用。 假如有下面这样一个场景:线程A获取到了锁,然后释放锁,然后再次获取锁。
第一步: 线程A执行了lock操作,获取到了锁,如下图:
上图的加锁操作中,线程A的当前CLHNode
节点的locked
状态被置为true
;然后tailNode
指针指向了当前线程的当前节点;最后因为前继节点的locked
状态为false
,不用自旋等待,因此获得了锁。
第二步: 线程A执行了unLock操作,释放了锁,如下图:
上图的释放锁操作中,线程A的当前CLHNode
节点的locked
状态被置为false
,表示释放了锁;然后新建了一个新的CLHNode
节点newCurNode
,线程A的当前节点线程本地变量值重新指向了newCurNode
节点对象。
第三步: 线程A再次执行lock操作,重新获得锁,如下图:
上图的再次获取锁操作中,首先将线程A的当前CLHNode
节点的locked
状态置为true
;然后首先通过tailNode
尾指针获取到前继节点即第一,二步中的curNode
对象,然后线程A的前继节点线程本地变量的值重新指向了重新指向了curNode
对象;然后tailNode
尾指针重新指向了新创建的CLHNode
节点newCurNode
对象。最后因为前继节点的locked
状态为false
,不用自旋等待,因此获得了锁。
扩展: 注意到以上图片的
preNode
对象此时没有任何引用,所以当下一次会被GC掉。前面是通过每次执行unLock
操作都新建一个新的CLHNode
节点对象newCurNode
,然后让线程A的当前节点线程本地变量值重新指向newCurNode
。因此这里完全不用重新创建新的CLHNode
节点对象,可以通过curNode.set(predNode.get());
这句代码进行优化,提高GC效率和节省内存空间。
4.5 考虑同个线程加锁释放锁再次获取锁异常的情况
现在我们把unLock
方法的CLHNode newCurNode = new CLHNode();
和curNode.set(newCurNode);
这两句代码注释掉,变成了下面这样:
// CLHLock.java
public void unLock() {
CLHNode node = curNode.get();
node.locked = false;
System.out.println(“线程” + Thread.currentThread().getName() + “释放了锁!!!”);
/CLHNode newCurNode = new CLHNode();
curNode.set(newCurNode);/
}
那么结果就是线程A通过加锁,释放锁后,再次获取锁时就会陷入自旋等待的状态,这又是为什么呢?我们下面来详细分析。
第一步: 线程A执行了lock操作,获取到了锁,如下图:
上图的加锁操作中,线程A的当前CLHNode
节点的locked
状态被置为true
;然后tailNode
指针指向了当前线程的当前节点;最后因为前继节点的locked
状态为false
,不用自旋等待,因此获得了锁。这一步没有什么异常。
第二步: 线程A执行了unLock操作,释放了锁,如下图:
现在已经把unLock
方法的CLHNode newCurNode = new CLHNode();
和curNode.set(newCurNode);
这两句代码注释掉了,因此上图的变化就是线程A的当前CLHNode
节点的locked
状态置为false
即!可。
第三步: 线程A再次执行lock操作,此时会陷入一直自旋等待的状态,如下图:
通过上图对线程A再次获取锁的lock
方法的每一句代码进行分析,得知虽然第二步中将线程A的当前CLHNode
的locked
状态置为false
了,但是在第三步线程A再次获取锁的过程中,将当前CLHNode
的locked
状态又置为true
了,且尾指针tailNode
指向的依然还是线程A的当前当前CLHNode
节点。又因为每次都是将尾指针tailNode
指向的CLHNode
节点取出来给当前线程的前继CLHNode
节点,之后执行while(predNode.locked) {}
语句时,此时因为predNode.locked = true
,因此线程A就永远自旋等待了。
5 测试CLH锁
下面我们通过一个Demo来测试前面代码实现的CLH锁是否能正常工作,直接上测试代码:
// CLHLockTest.java
/**
- 用来测试CLHLocke生不生效
- 定义一个静态成员变量cnt,然后开10个线程跑起来,看能是否会有线程安全问题
*/
public class CLHLockTest {
private static int cnt = 0;
public static void main(String[] args) throws Exception {
final CLHLock lock = new CLHLock();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
lock.lock();
cnt++;
lock.unLock();
}).start();
}
// 让main线程休眠10秒,确保其他线程全部执行完
Thread.sleep(10000);
System.out.println();
System.out.println(“cnt----------->>>” + cnt);
}
}
下面附运行结果截图:
PS: 这里为了截图全面,因此只开了10个线程。经过劲越测试,开100个线程,1000个线程也不会存在线程安全问题。
6 小结
好了,前面我们通过多图详细说明了CLH锁的原理与实现,那么我们再对前面的知识进行一次小结:
大家看完有什么不懂的可以在下方留言讨论.
谢谢你的观看。
觉得文章对你有帮助的话记得关注我点个赞支持一下!
链接:https://juejin.im/post/6864210697292054541
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

最后
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
/img-community.csdnimg.cn/images/e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />
最后
[外链图片转存中…(img-AjnlffIe-1713270998937)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!