美文网首页
JUC-(9)CLHLock实现

JUC-(9)CLHLock实现

作者: 一个菜鸟JAVA | 来源:发表于2020-07-21 18:08 被阅读0次

简介

在AQS的源码中有介绍到一种叫CLH的队列(AQS中使用的是一种变种).CLH它是一种基于单向链表实现的队列,即后一个节点保存前一个节点的引用形成单向链接.我们可以使用CLH来实现一个高效的自旋锁.每个线程请求锁时,都先判断它的前节点是否需要锁,如果前节点需要锁则自己自旋等待.如果前节点不需要锁则获取锁成功.
正式因为它的这种实现方式,它具有先来先获取的公平性.而它的核心基于一个CAS操作来实现具有很高的性能.但因为其自旋等待,所以不太适合占用锁时间过长的场景,因为自旋是会需要消耗CPU资源的.

简单实现

/**
 * 声明锁接口
 */
public interface Lock {
    /**
     * 获取锁
     */
    void lock();

    /**
     * 解锁
     */
    void unlock();
}

/**
 * 定义Node
 */
public class CLHNode {
    /**
     * 使用volatile修饰保证可见性.
     */
    volatile boolean locked;
}

/**
 * CLHLock实现
 */
public class CLHLock implements Lock{
    /**
     * 保存当前节点,一个线程一个
     */
    private final ThreadLocal<CLHNode> node;
    /**
     * 尾节点.所有线程共享一个.支持CAS操作
     */
    private final AtomicReference<CLHNode> tail;
    /**
     * 父节点.一个线程一个
     */
    private final ThreadLocal<CLHNode> pred;

    public CLHLock() {
        this.node = ThreadLocal.withInitial(CLHNode::new);
        this.tail = new AtomicReference<>(new CLHNode());
        this.pred = new ThreadLocal<>();
    }

    @Override
    public void lock() {
        //获取当前节点
        CLHNode current = node.get();
        //将当前节点设置成需要锁
        current.locked = true;
        //将当前节点设置成尾节点,CAS操作
        CLHNode currentPred = tail.getAndSet(current);
        //设置当前节点的前节点
        pred.set(currentPred);
        //查看当前节点是否需要锁,如果需要则自旋等待
        while (currentPred.locked){

        }
        //获取锁成功
        System.out.println(Thread.currentThread().getName()+":获取到锁");
    }

    @Override
    public void unlock() {
        //获取当前节点
        CLHNode current = node.get();
        //将当前节点locked设置成false,它后面的节点将会推出自旋
        current.locked = false;
        //将当前节点设置成前节点
        node.set(pred.get());
    }
}

/**
 * 使用测试
 */
public class App {

    public static void main(String[] args) {
        Lock lock = new CLHLock();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            }
        };

        Thread t1 = new Thread(runnable);
        t1.setName("t1");
        t1.start();

        Thread t2 = new Thread(runnable);
        t2.setName("t2");
        t2.start();

        lock.lock();
        lock.unlock();

    }
}
  • 获取锁:每个线程获取锁时都自己节点的locked设置成true代表自己需要锁,然后将自己节点设置成尾节点tail(这里使用AtomicReference来实现一个CAS操作),并获取到当前节点的前置节点.接着获取的前置节点设置成自己的前置节点.然后就是判断通过前置节点的locked属性来判断前置节点是否需要锁.这里需要注意的是locked属性需要使用volatile修饰来保证其他线程修改它的值而当前线程能感知到该值的变化.如果去掉volatile可能导致当前线程一直自旋无法获取到锁.
  • 释放锁:释放锁就比较好理解了,直接获取到当前节点(这就是为什么使用ThreadLocal来保存当前节点的值了).然后将当前节点的locked设置成false,而这个操作将会使该节点的后置节点退出自旋获取到锁.

关于解锁操作中的node.set(pred.get()).它主要是为了防止获取锁解锁之后再次获取锁无法获取锁的bug.如果去掉该行代码,第一次获取锁当前节点已经是tail节点了,然后再次获取锁.再次将当前节点作为尾节点.即当前节点的头节点和当前节点是同一个节点.如果这个时候设置lockedtrue,这将导致当前节点再判断前节点是否需要锁时一直都是true.线程将在获取锁时无限自旋等待.

总结

总体来说使用这种方式的有点如下:

  • 简单高效.
  • 支持FIFO公平性.
  • 自旋等待避免线程切换的性能损耗

同样它也有下面的缺点:

  • 不可重入.即获取锁之后必须释放锁才能再次获取锁.
  • 锁占用时间过长导致CPU空转.

相关文章

  • JUC-(9)CLHLock实现

    简介 在AQS的源码中有介绍到一种叫CLH的队列(AQS中使用的是一种变种).CLH它是一种基于单向链表实现的队列...

  • JUC-死锁

      死锁:两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉它们都将无法推进下去...

  • JUC-(8)ReentrantReadWriteLock

    ReentrantReadWriteLock是一个读写锁,与ReentrantLock不同之处在于它提供两种模式的...

  • JUC-(2)BlockingQueue

    Queue Queue属于集合.它除了支持集合的基本操作,同时还支持插入,获取和检查存在操作. 操作抛出异常返回特...

  • JUC-(7)ReentrantLock

    阅读建议 在阅读本文前建议先了解synchronized和wait/notify机制,这样能提高你的阅读效率加快理...

  • JUC-阻塞队列

      阻塞队列数据结构示意图如下所示: 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞; 当阻塞队列是满时,往队...

  • JUC-传值

      栈管运行,堆管存储。传值示例如下:   字符串常量池-位于Java堆(jdk1.7以后)。如下代码创建了几个对象:

  • JUC-工具类

    Semaphore 信号量或者说叫管理器,它其实不是对资源加锁,而是控制最多N个线程可以同时访问资源。类似停车场,...

  • JUC-队列源码解析

    PriorityQueue 我们先看下PriorityQueue的继承结构 先看些接口Queue Abstract...

  • Python练习3

    用函数实现9*9乘法口诀

网友评论

      本文标题:JUC-(9)CLHLock实现

      本文链接:https://www.haomeiwen.com/subject/aljwkktx.html