多线程笔记(十四):AQS 原理分析

1.什么是AQS

       AQS,全称 AbstractQueuedSynchronizer,位于 java.util.concurrent.locks 包下。

       是JDK 1.5提供的一套用于实现阻塞锁和一系列依赖FIFO等待队列的同步器(First Input First Output   先进先出)的框架实现。

       是除了java自带的 synchronized 关键字之外的锁机制。

       我们常用的 ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的。具体用法是通过继承AQS,并实现其模板方法,来达到同步状态的管理。

       可以将 AQS 作为一个队列来理解。

2.功能划分

      AQS的功能在使用中可以分为两种:独占锁共享锁

  1. 独占锁:每次只能有一个线程持有锁。eg:ReentrantLock就是独占锁
  2. 共享锁:允许多个线程同时获得锁,并发访问共享资源。eg:ReentrantReadWriteLock中的读锁、CountDownLatch

3.AQS原理分析

       AQS分析,在 AbstractQueuedSynchronizer 类中,有一个静态内部类 Node,如下图所示

      

       此处,静态内部类 Node 相当于一个包装类。Node节点,实际上是一个双向链表的结构。

       Node 节点的实际原理是:如果当前线程执行时发现,已经有其他线程获得锁,那么 AQS 会将当前阻塞线程包装成一个 Node 节点,添加到同步队列中,同时会让当前线程阻塞(此处使用的是LockSupport.park()阻塞的)

 Node 主要属性

static final class Node {
    //表示节点的状态,包含 SIGNAL、CANCELLED、CONDITION、PROPAGATE、INITIAL
    volatile int waitStatus;
    //前继节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //当前线程
    volatile Thread thread;
    //存储在 condition 队列中的后继节点
    Node nextWaiter;
}

节点状态介绍

SIGNAL:值为-1,后继节点的线程处于等待状态,如果当前节点的线程释放了同步状态或者被取消时,将会通知后继节点,使后继节点得以运行。

CANCELLED:值为1,由于在同步队列中等待的线程等待超时、被中断时,需要从同步队列中取消等待,节点进入该状态将不会变化。

CONDITION:值为-2,节点在等待队列中,节点线程等待在 Condition 上,当其他线程对 Condition 调用了 signal()方法后,该节点将会从等待队列中转移至同步队列中,加入到对同步状态的获取中来。

PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件的被传播下去。

INITIAL:值为0,初始状态。

备注:节点状态源码分析处会有使用到。signal()方法类似于 Thread.notify()方法

AQS中两个主要属性

  AQS中有一个被 volatile 修饰,并且不可序列化的两个属性:head 和 tail(即:链表的头部和尾部)

/**
 * Head of the wait queue, lazily initialized. 
*/
private transient volatile Node head;

/*
 * Tail of the wait queue, lazily initialized. .
 */
private transient volatile Node tail;

图示(Node结构 && AQS同步器)

  

图片解析

       AQS中有多个线程,在同步器中等待,如下图所示(链表结果,前一个 Node节点 的 next 指向后一个 Node,后一个Node节点 prev 指向前一个Node。节点为空,则指向为 null)

       当前线程获得同步锁后,其他线程是无法获取到锁的。此时没有获得同步锁的线程,会被以 Node 节点的形式加入到上图中的同步队列中。

       但是在加入的过程中,必须要保证线程的安全性。如果在加入的过程中,多个线程都在同步竞争这把锁,那么此时则会存在安全问题。针对此处的安全问题,使用到了 CAS(后续会有介绍)。

       如果需要把一个未获得锁的线程,加入到一个AQS环形链表尾部的话,此处会使用到 compareAndSetTail()方法,来把一个Node 节点加入到环形链表的尾部。

/**
 * CAS tail field. Used only by enq.
 */
private final boolean compareAndSetTail(Node expect, Node update) {
	return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

       如果是初次(此时链表为空,即链表中没 Node 节点)往链表中添加节点,此处会使用到 compareAndSetHead() 方法,来把一个 Node 节点加入到换新链表的头部。

/**
 * CAS head field. Used only by enq.
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

       通过 CAS 操作,线程一定是安全的。CAS操作成功,此时节点就会加入到环形同步链表中了。环形链表释放锁的过程,请继续往下看。

链表释放锁过程

       head 首节点在释放锁时,会唤醒后续的节点(即:next 指向的节点),后续节点此时会尝试去获取锁。获取锁的过程,就是通过上面描述的叫做 compareAndSetHead() 的方法。该方法会将该节点设置成头结点,如果设置头结点成功的话,则说明当前节点获得了这把锁。

       切记:首节点在释放锁的同时,同时会取消与下一节点之间的关联关系,并被回收。最终状态即 AQS 的 head 和 tail 均指向最后一个 Node 节点(如下图所示,以3个节点为例)

  

     接下来将会从AQS 过渡到 CAS,从CAS层面来分析,如何保证线程的安全性

CAS原理(源码)分析

        在 AQS 中,除了本身的链表结构以外,还有一个很关键的功能,那就是CAS。它能够解决线程在高并发情况下的线程安全问题。接下来我们就从 compareAndSetXXX() 方法入手分析 。

/**
 * CAS head field. Used only by enq.
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

/**
 * CAS tail field. Used only by enq.
 */
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

1.Unsafe类     

       在 AQS 设置首节点和为节点的方法中,我们发现它们调用的都是 unsafe.compareAndSwapObject() 方法。那么我们先来普及一下 Unsafe 类。

       Unsafe 类是在 sun.misc 包下,并不属于 Java 标准,但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类来开发的,比如:Netty、Hadoop、Kafka 等。

       我们可以认为 Unsafe 类是 Java 中留下的后门。在 JDK 层面上定义的这么一个 Unsafe 类,它提供了一下偏底层操作,可以调用 JVM 层面上的方法。如:使用Unsafe类可以①直接访问内存  ②线程调度 等。

2.unsafe.compareAndSwapObject() 分析

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

       我们发现 compareAndSwapObject() 方法,是一个由 native 修饰的方法。Native Method 就是一个 java 调用非 java 代码的接口。这个也比较好理解,因为 java 底层本来就是用 C/C++ 来开发的,所以当然有对应接口去直接调用C/C++写的方法。众所周知 java 对底层的操作远不如 C/C++ 灵活,所以可以通过直接调用非 java 代码来实现对底层的操作。

       参考 compareAndSetTail()方法,来分析一下 compareAndSwapObject() 方法具体传递的参数情况吧。

private final boolean compareAndSetTail(Node expect, Node update) {
    //this:需要改变的对象
    //tailOffset:偏移量
    //expect:期望值
    //update:更新后的值
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

        此处 tailOffset 偏移量,具体又是个什么东西呢?我们接下来继续深入,分析 tailOffset 参数。

3.tailOffset/headOffset 偏移量参数分析

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
    try {
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) { throw new Error(ex); }
}

       我们发现偏移量这块,调用的也是 Unsafe 类。通过调用 Unsafe 类中的 objectFieldOffset() 方法,通过反射的方式去获得当前成员变量的内存地址。

       那内存地址又是用来干嘛的呢?

       任何一个类中的字段,都是会按照一定的顺序在内存中有一个地址进行存放。然后通过 unsafe.objectFieldOffset() 方法,我们可以准确的知道 head、tail 等这些对象字段在内存地址中的偏移量。

       那偏移量又是干嘛的呢?

       JVM 会根据偏移量来找到该对象(成员变量)在内存中的具体位置。


       分析完偏移量之后,我们回到 unsafe.compareAndSwapObject() 方法。如果想要继续了解 native 定义的方法,那么你则需要进入到 JVM 层面进行分析,此处不再继续介绍。我们只要知道 compareAndSetHead()、compareAndSetTail() 方法的大致操作就可以了。


AQS 原理分析,介绍到此为止

如果本文对你有所帮助,那就给我点个赞呗 ^_^ 

End

发布了247 篇原创文章 · 获赞 44 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/lzb348110175/article/details/103709548