• Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁


    上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简:

    CLH lock queue其实就是一个FIFO的队列,队列中的每个结点(线程)只要等待其前继释放锁就可以了。

    AbstractQueuedSynchronizer是通过一个内部类Node来实现CLH lock queue的一个变种,但基本原理是类似的。

    在介绍Node类之前,我们来介绍下Spin Lock,通常就是用CLH lock queue来实现自旋锁,所谓自旋锁简单来说就是线程通过循环来等待而不是睡眠。 Talk 再多不如 show code:

    class ClhSpinLock {
        private final ThreadLocal<Node> prev;
        private final ThreadLocal<Node> node;
        private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
    
        public ClhSpinLock() {
            this.node = new ThreadLocal<Node>() {
                protected Node initialValue() {
                    return new Node();
                }
            };
    
            this.prev = new ThreadLocal<Node>() {
                protected Node initialValue() {
                    return null;
                }
            };
        }
    
        public void lock() {
            final Node node = this.node.get();
            node.locked = true;
            // 一个CAS操作即可将当前线程对应的节点加入到队列中,
            // 并且同时获得了前继节点的引用,然后就是等待前继释放锁
            Node pred = this.tail.getAndSet(node);
            this.prev.set(pred);
            while (pred.locked) {// 进入自旋
            }
        }
    
        public void unlock() {
            final Node node = this.node.get();
            node.locked = false;
            this.node.set(this.prev.get());
        }
    
        private static class Node {
            private volatile boolean locked;
        }
    }

    上面的代码中线程巧妙的通过ThreadLocal保存了当前结点和前继结点的引用,自旋就是lock中的while循环。 总的来说这种实现的好处是保证所有等待线程的公平竞争,而且没有竞争同一个变量,因为每个线程只要等待自己的前继释放就好了。 而自旋的好处是线程不需要睡眠和唤醒,减小了系统调用的开销。

    public static void main(String[] args) {
        final ClhSpinLock lock = new ClhSpinLock();
        lock.lock();
    
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    System.out.println(Thread.currentThread().getId() + " acquired the lock!");
                    lock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
    
        System.out.println("main thread unlock!");
        lock.unlock();
    }

    上面代码的运行的结果应该跟上一篇文章中的完全一样。

    ClhSpinLock的Node类实现很简单只有一个布尔值,AbstractQueuedSynchronizer$Node的实现稍微复杂点,大概是这样的:

         +------+  prev +-----+       +-----+
    head |      | <---- |     | <---- |     |  tail
         +------+       +-----+       +-----+
    • head:头指针
    • tail:尾指针
    • prev:指向前继的指针
    • next:这个指针图中没有画出来,它跟prev相反,指向后继

    关键不同就是next指针,这是因为AQS中线程不是一直在自旋的,而可能会反复的睡眠和唤醒,这就需要前继释放锁的时候通过next 指针找到其后继将其唤醒,也就是AQS的等待队列中后继是被前继唤醒的。AQS结合了自旋和睡眠/唤醒两种方法的优点。

    其中线程的睡眠和唤醒就是用到我下一篇文章将要讲到的LockSupport

    最后提一点,上面的ClhSpinLock类中还有一个关键的点就是lock方法中注释的地方:

    一个CAS操作即可将当前线程对应的节点加入到队列中,并获取到其前继。

    实际上可以说整个AQS框架都是建立在CAS的基础上的,这些原子操作是多线程竞争的核心地带,AQS中很多绕来绕去的代码都是为了 减少竞争。我会在后面AbstractQueuedSynchronizer源码分析中做详细介绍。

  • 相关阅读:
    Phonon
    qt 的mysql的库
    vwmare下安装fedora
    C++标准库
    C#命名空间
    用谷歌Chrome浏览器来当手机模拟器
    Javascript实现ECMAScript 5中的map、reduce和filter函数
    页面变灰实现方案
    jQuery检查元素是否在视口内(屏幕可见区域内)
    兼容浏览器的获取指定元素(elem)的样式属性(name)的方法
  • 原文地址:https://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-clh-and-spin-lock.html
Copyright © 2020-2023  润新知