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源码分析中做详细介绍。

时间: 2024-12-25 16:55:42

Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁的相关文章

Java并发包源码学习之AQS框架(一)概述

AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.concurrent的核心之一,也可以说是阅读整个并发包源码的一个突破口. 比如读ReentrantLock的源码你会发现其核心是它的一个内部类Sync: 整个包中很多类的结构都是如此,比如Semaphore,CountDownLatch都有一个内部类Sync,而所有的Sync都是继承自AbstractQ

Java并发包源码学习之线程池(一)ThreadPoolExecutor源码分析

Java中使用线程池技术一般都是使用Executors这个工厂类,它提供了非常简单方法来创建各种类型的线程池: public static ExecutorService newFixedThreadPool(int nThreads) public static ExecutorService newSingleThreadExecutor() public static ExecutorService newCachedThreadPool() public static Scheduled

深入java并发包源码(二)AQS的介绍与使用

深入java并发包源码(一)简介 深入java并发包源码(二)AQS的介绍与使用 深入java并发包源码(三)AQS独占方法源码分析 AQS 本文章会讲解 AQS 的使用方法,然后通过 DEBUG 跟踪 AQS 执行的一系列操作来分析源码,读者跟着文章 DEBUG 跟踪源码能更容易理解. AQS 是什么? AbstractQueuedSynchronizer 队列同步器(AQS)是一个抽象类,作为并发工具的基础组件,为真正的实现类提供基础设施.并发工具是面向使用者的,AQS 面向的是并发工具的实

深入java并发包源码(一)简介

深入java并发包源码(一)简介 深入java并发包源码(二)AQS的介绍与使用 深入java并发包源码(三)AQS独占方法源码分析 阅读本文章前需要了解 CAS 操作是什么. 首先大致介绍一下需要讲到的几个类,只需要理解这几个类是什么关系即可,后面会有详细解析. Unsafe :这个类提供了 native 方法,未开源,提供了线程阻塞和唤醒,原子操作等方法. LockSupport :包装了一层 Unsafe 类,非常类似于代理者模式,将在 Unsafe 类中的线程挂起唤醒等操作导出,避免将

【JDK1.8】 Java小白的源码学习系列:HashMap

目录 Java小白的源码学习系列:HashMap 官方文档解读 基本数据结构 基本源码解读 基本成员变量 构造器 巧妙的tableSizeFor put方法 巧妙的hash方法 JDK1.8的putVal方法 JDK1.8的resize方法 初始化部分 数组搬移部分 Java小白的源码学习系列:HashMap 春节拜年取消,在家花了好多天时间啃一啃HashMap的源码,同样是找了很多很多的资料,有JDK1.7的,也有JDK1.8的,当然本文基于JDK1.8.将所学到的东西进行整理,希望回过头再看

struts2源码学习之初始化(二)

在上一篇struts2源码学习之初始化(一)中,详细描述了StrutsPrepareAndExecuteFilter的init()的主要工作,这一篇就详细说说Dispatcher.从上一篇文章中,我们知道了Dispatcher在Filter的init()方法中被创建出来,那么,它的功能是什么呢?Dispatcher类的功能正如它的名字所示,是派发,派发请求. PrepareOperations类预处理请求,比如找到findActionMapping(),找到之后就要交给Dispatcher,让D

Java并发包源码分析

并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善.现代的PC都有多个CPU或一个CPU中有多个核,是否能合理运用多核的能力将成为一个大规模应用程序的关键. Java基础部分知识总结点击Java并发基础总结.Java多线程相关类的实现都在Java的并发包concurrent,concurrent包主要包含3部分内容,第一个是atomic包,里面主要是一些原子类,比如AtomicInteger.

java.util.Objects 源码学习

Objects 与 Object 区别 Object 是 Java 中所有类的基类,位于java.lang包. Objects 是 Object 的工具类,位于java.util包.它从jdk1.7开始才出现,被final修饰不能被继承,拥有私有的构造函数. 它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode.返回对象的字符串表示形式.比较两个对象. Objects 各方法介绍与分析 equals

java进阶(六)------源码学习---myeclipse如何查看jar包的源码

查看源码的思路和代码规范是我们学习完善代码编写能力的重要手段. 有时候我们会遇到想看某个jar包中的类和方法的实现,但是无法查看. 这是因为未加载jar包的源码.只要找到jar包的源码并把路径设置好 就可以查看了. 源码下载 源码的下载 需要自己按照版本找好,可以是zip也可以是jar包. 这里有几个找源码的网站,也可以去jar包的官网查找. github https://github.com/openjdk-mirror/jdk7u-jdk sourceforge https://source