ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

-So-easy!多图详解CLH锁的原理与实现,轻松把握AQS,面试复盘

2021-09-11 22:02:15  阅读:231  来源: 互联网

标签:CLH AQS 获取 多图 线程 当前 CLHNode locked 节点


private static class CLHNode {
    // 锁状态:默认为false,表示线程没有获取到锁;true表示线程获取到锁或正在等待
    // 为了保证locked状态是线程间可见的,因此用volatile关键字修饰
    volatile boolean locked = false;
}
// 尾结点,总是指向最后一个CLHNode节点
// 【注意】这里用了java的原子系列之AtomicReference,能保证原子更新
private final AtomicReference<CLHNode> tailNode;
// 当前节点的前继节点
private final ThreadLocal<CLHNode> predNode;
// 当前节点
private final ThreadLocal<CLHNode> curNode;

// CLHLock构造函数,用于新建CLH锁节点时做一些初始化逻辑
public CLHLock() {
    // 初始化时尾结点指向一个空的CLH节点
    tailNode = new AtomicReference<>(new CLHNode());
    // 初始化当前的CLH节点
    curNode = new ThreadLocal() {
        @Override
        protected CLHNode initialValue() {
            return new CLHNode();
        }
    };
    // 初始化前继节点,注意此时前继节点没有存储CLHNode对象,存储的是null
    predNode = new ThreadLocal();
}

/**
 * 获取锁
 */
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) {
        System.out.println("线程" + Thread.currentThread().getName() + "没能获取到锁,进行自旋等待。。。");
    }
    // 能执行到这里,说明当前线程获取到了锁
    System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁!!!");
}

/**
 * 释放锁
 */
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());
}

}


# 4.1 CLH锁的初始化逻辑

通过上面代码,我们缕一缕CLH锁的初始化逻辑先:

1.  定义了一个`CLHNode`节点,里面有一个`locked`属性,表示线程线程是否获得锁,默认为`false`。`false`表示线程没有获取到锁或已经释放锁;`true`表示线程获取到了锁或者正在自旋等待。

    > 注意,为了保证`locked`属性线程间可见,该属性被`volatile`修饰。

2.  `CLHLock`有三个重要的成员变量尾节点指针`tailNode`,当前线程的前继节点`preNode`和当前节点`curNode`。其中`tailNode`是`AtomicReference`类型,目的是为了保证尾节点的线程安全性;此外,`preNode`和`curNode`都是`ThreadLocal`类型即线程本地变量类型,用来保存每个线程的前继`CLHNode`和当前`CLHNode`节点。
3.  最重要的是我们新建一把`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() + “获取到了锁!!!”);
    }


虽然代码的注释已经很详细,我们还是缕一缕线程加锁的过程:

1.  首先获得当前线程的当前节点`curNode`,这里每次获取的`CLHNode`节点的`locked`状态都为`false`;
2.  然后将当前`CLHNode`节点的`locked`状态赋值为`true`,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态;
3.  因为尾指针`tailNode`的总是指向了前一个线程的`CLHNode`节点,因此这里利用尾指针`tailNode`取出前一个线程的`CLHNode`节点,然后赋值给当前线程的前继节点`predNode`,并且将尾指针重新指向最后一个节点即当前线程的当前`CLHNode`节点,以便下一个线程到来时使用;
4.  根据前继节点(前一个线程)的`locked`状态判断,若`locked`为`false`,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待。

为了更通俗易懂,我们用一个图里来说明。

**假如有这么一个场景:**有四个并发线程同时启动执行lock操作,假如四个线程的实际执行顺序为:threadA<--threadB<--threadC<--threadD

**第一步**,线程A过来,执行了lock操作,获得了锁,此时`locked`状态为`true`,如下图:

![](https://upload-images.jianshu.io/upload_images/24195226-0485c483260cd736.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)



**第二步**,线程B过来,执行了lock操作,由于线程A还未释放锁,此时自旋等待,`locked`状态也为`true`,如下图:

![](https://upload-images.jianshu.io/upload_images/24195226-a77032844c086d02.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


**第三步**,线程C过来,执行了lock操作,由于线程B处于自旋等待,此时线程C也自旋等待(因此CLH锁是公平锁),`locked`状态也为`true`,如下图:![](https://upload-images.jianshu.io/upload_images/24195226-690806a461e097b2.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


**第四步**,线程D过来,执行了lock操作,由于线程C处于自旋等待,此时线程D也自旋等待,`locked`状态也为`true`,如下图:![](https://upload-images.jianshu.io/upload_images/24195226-75723583476afbf9.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


这就是多个线程并发加锁的一个过程图解,当前线程只要判断前一线程的`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());
    }




> **Java网盘:pan.baidu.com/s/1MtPP4d9Xy3qb7zrF4N8Qpg
> 提取码:2p8n**



# 总结

对于面试还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)

![image](https://www.icode9.com/i/ll/?i=img_convert/528918d131eb2ae6a0aab3e953fb8496.png)

**[CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】](

)**

**Java面试精选题、架构实战文档**

**整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~**

于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)

[外链图片转存中...(img-CYh8VYvn-1631368330410)]

**[CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】](

)**

**Java面试精选题、架构实战文档**

**整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~**

**你的支持,我的动力;祝各位前程似锦,offer不断!**

标签:CLH,AQS,获取,多图,线程,当前,CLHNode,locked,节点
来源: https://blog.csdn.net/m0_60707660/article/details/120243861

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有