Java AQS核心数据结构CLH锁以及AQS中对其的改进

1、自旋锁

1.1、什么是自旋锁

CLH锁是对自旋锁的一种改进。先看看什么是自旋锁,自旋锁是互斥锁的一种体现,Java实现如下:

public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<Thread>();

    public void lock() {
        Thread currentThread = Thread.currentThread();
        // 如果锁未被占用,则设置当前线程为锁的拥有者
        while (!owner.compareAndSet(null, currentThread)) {
        }
    }

    public void unlock() {
        Thread currentThread = Thread.currentThread();
        // 只有锁的拥有者才能释放锁
        owner.compareAndSet(currentThread, null);
    }
}

自旋锁在获取锁时,线程会对一个原子变量循环执行compareAndSet方法,直到该方法返回成功即成功获取锁。compareAndSet操作是通过CAS实现的,因此该操作是原子操作。原子性保证了根据最新消息计算出新值,如果与此同时值已由另一个线程更新,则写入失败。因此,这段代码可以实现互斥锁的功能。

1.2、自旋锁的优缺点

优点:

  1. 实现简单,避免了操作系统进程调度和线程上下文切换的开销。

缺点:

  1. 存在锁饥饿现象。在锁竞争激烈的情况下,可能存在一个线程一直被其他线程“插队”而一直获取不到锁的情况。
  2. 性能问题,在实际的多处理上运行的自旋锁在锁竞争激烈时性能较差。

自旋锁适用于锁竞争不激烈、锁持有时间短的场景。

2、CLH 锁

2.1、CLH锁介绍

CLH锁是对自旋锁的一种改进,由Craig、Landin和Hagersten(CLH)发明,有效的解决了以上的两个缺点:

  1. 将线程组织成一个队列,保证先请求的线程先获得锁,避免了饥饿问题。
  2. 锁状态去中心化,让每个线程在不同的状态变量中自旋。当一个线程释放它的锁时,只能使其后续线程缓存的信息失效,从而去获取锁,避免了惊群现象,从而减少了CPU的开销。

CLH锁的数据结构类似一个链表结构(实现时并不是真实的链表,而是一种隐式的链表队列),所有请求获取锁的线程会排列在链表队列中,每个节点会自旋访问其前一个节点的状态。当前一个节点释放锁时,只有它的后一个节点才可以得到锁。

详细来说:CLH锁本身有一个对位指针Tail,其是一个原子变量,指向队列最末端的CLH节点。每一个CLH节点有两个属性:所代表的线程和标识是否持有锁的状态变量。当一个线程(记为Th1)要获取锁时,它会对Tail进行一个getAndSet的原子操作,该操作会返回Tail指向的节点(也就是队尾节点,记为C0),并将其作为Th1所对应的CLH节点(记为C1)的前驱节点,最后将Tail指向C1,使其称为新的队尾节点。入队成功后,C1就会自旋访问其上一个节点C0的状态变量,当上一个节点C0释放锁后,C1将得到这个锁。如下图所示:

在这里插入图片描述

2.2、CLH锁的代码分析

public class CLHLock {
    private final ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);
    private final AtomicReference<Node> tail = new AtomicReference<>(new Node());
    
    private static class Node {
        private volatile boolean locked;
    }
    
    public void lock(){
        Node node = this.node.get();
        node.locked = true;
        Node pre = this.tail.getAndSet(node);
        while (pre.locked);
    }
    
    public void unlock(){
        final Node node = this.node.get();
        node.locked = false;
        this.node.set(new Node());
    }
}

加锁过程:

  1. 首先获取当前线程的节点node,该节点的locked被初始化为false,并且在获取并释放锁后也会被设置为false。
  2. 将节点node的状态locked状态设置为true,标识其正在等待获得锁或已经获得了锁。
  3. 操作Node pre = this.tail.getAndSet(node); 先将尾节点取出来作为当前节点node的前一个节点Pre,然后把当前节点node作为尾节点。getAndSet是一个原子操作。
  4. 自旋等待前一个节点释放锁。若前一个节点Pre释放了锁,那么Pre的locked状态将更改为false,循环结束,当前节点node获得锁成功。

解锁过程:

  1. 首先从ThreadLocal获取当前线程的CLH节点node。
  2. 将其节点node的状态locked设置为false,代表其释放了锁。(此时其下一个节点就会结束自旋等待去获取锁,与加锁过程的第四步呼应)。
  3. 向ThreadLocal中重新赋值一个新的CLH节点。这步操作是为了避免node被复用,导致死锁。

疑问:

1、CLH是一个链表队列,为什么Node节点没有指向前驱或后继的指针?

CLH锁是一种隐式的链表队列,没有显式的维护前驱或后继指针。因为每个等待获取锁的线程只需要自旋等待前一个节点的状态就好了,而不需要遍历整个队列。因此只需要使用一个局部变量保存前驱节点,不需要显式维护前驱或后继指针。

2、在解锁过程的第三步中,为什么需要重新向ThreadLocal中重新赋值一个新的CLH节点?

  • 起初,C1持有锁,C2自旋等待。
  • 当C1释放锁,将状态locked设置为false,此时C2就会去抢夺锁,但是并未抢到就到了下一步。
  • 假如此时C1又去调用lock()获得锁,会将状态设置会true,并将其的前驱节点设置为C2。由于C1是C2的前驱节点,C2还一直自旋等待C1释放锁,同时C2是C1的前驱节点,C1也在等待C2释放锁,因此出现了死锁 ,如下图所示。
  • 当在ThreadLocal中重新赋值一个新的CLH节点后,C1在将节点状态释放变为false后,即使其再次调用lock()获得锁,将状态locked设置为true的锁节点并不是原来的节点(C2的前驱节点),因此C2会正常结束自旋等待获得到锁。

因此需要在ThreadLocal中重新赋值一个新的CLH节点,避免Node节点复用带来的死锁问题。

在这里插入图片描述

2.3、优缺点分析

优点:

  1. 性能好,获取和释放锁的开销小,CLH的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。
  2. 公平锁,先入队的线程会先得到锁,避免出现锁饥饿现象。
  3. 实现简单,扩展性强。

缺点:

  1. 有自旋操作,当锁持有时间长时会带来较大的CPU开销。
  2. 基本的CLH锁功能单一,不能支持复杂的功能(AQS中使用CLH对基本的CLH进行了一定的改造)。

3、AQS对CLH队列锁的改进

针对CLH的缺点,AQS对CLH队列锁进行了改造:

  • 针对第一个缺点,AQS将自旋操作改为阻塞线程操作(使用park/unpark)。
  • 针对第二个缺点,AQS对CLH锁进行了改造和扩展:1、扩展每个节点的状态。2、显示维护前驱节点和后继节点。3、将出队节点显示设置为null辅助GC的优化等。

3.1、扩展每个节点的状态

AQS中由waitStatus变量保存节点状态。

volatile int waitStatus;

AQS提供了该状态变量的原子读写操作,AQS中的节点状态有以下五种:

状态名描述
CANCELLED值为1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待。
SIGNAL值为-1,后续节点处于等待状态,当前节点地线程释放了锁(同步状态)或者被取消,会通知后继节点。
CONDITION值为-2,该节点位于条件队列中,并不在同步队列中。当其他线程调用该condaition的signal方法后,才会加入到同步队列中。
PROPAGATE值为-3,表示下一次共享式同步状态获取将会无条件地传播下去。
INITIAL值为0,初始状态。

3.2、显示维护前驱节点和后继节点

AQS中使用阻塞等待替换了自旋操作,线程会阻塞等待锁的释放,不能主动感知到前驱节点的状态变化。因此AQS中显式维护了前驱节点和后继节点,需要释放锁的节点会显式通知下一个节点解除阻塞。

3.3、辅助GC

JVM 的垃圾回收机制使开发者无需手动释放对象。但在 AQS 中需要在释放锁时显式的设置为 null,避免引用的残留,辅助垃圾回收。

参考:

[1]https://zhuanlan.zhihu.com/p/398582011

[2]https://blog.csdn.net/fengyuyeguirenenen/article/details/123856507

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值