多线程/并发编程——两万字详解AQS

多线程/并发编程——并发锁的底层实现AQS详解

在前面我们学习 Reentrantlock 的时候,我们知道 Reentrantlock 的底层实现是 AQS,实际上 AQS 是 Java 语言提供的并发工具包 J.U.C 的实现核心,不止 Reentrantlock ,后面要讲的 CountDownLatchCyclicBarrier 等并发工具类都是基于 AQS 实现的,下面我们就来详细学习一下 AQS 的底层实现原理

一、AQS概要

1、AQS 概念

AQS 即 AbstractQueuedSynchronizer,又称为队列同步器,AQS 是 JDK 下提供的一套用于实现基于 FIFO 等待队列的阻塞锁和相关的同步器的一个同步框架,它是同步工具类 J.U.C 的底层实现

AQS 抽象类的定义:

/**
 * AQS抽象类
 */
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    
    
//指向同步队列队头
private transient volatile Node head;

//指向同步的队尾
private transient volatile Node tail;

//表示共享资源的同步状态,0代表锁未被占用,>= 1代表锁已被占用
private volatile int state;

//省略其他代码......
}

AQS 是一个抽象类,其作用等同于接口,在内部定义了一些标准的方法,其具体含义由子类实现而定。在 AQS 中主要定义了如下模板方法:

//AQS中提供的主要模板方法,由子类实现。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    
    

    //独占模式下获取锁的方法
    protected boolean tryAcquire(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //独占模式下解锁的方法
    protected boolean tryRelease(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //共享模式下获取锁的方法
    protected int tryAcquireShared(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //共享模式下解锁的方法
    protected boolean tryReleaseShared(int arg) {
    
    
        throw new UnsupportedOperationException();
    }
    //判断是否为持有独占锁
    protected boolean isHeldExclusively() {
    
    
        throw new UnsupportedOperationException();
    }

}

以上方法不需要全部实现,根据获取的锁的种类可以选择实现不同的方法:

  • 支持独占(排他)获取锁的同步器应该实现tryAcquiretryReleaseisHeldExclusively
  • 支持共享获取的同步器应该实现tryAcquireSharedtryReleaseSharedisHeldExclusively

比如 ReentrantLock 既可以实现公平锁,也可以实现公平锁,其底层实现就是通过重写如上的所有方法,关于 ReentrantLock 我们在下文将要详细讲述

2、AQS 的同步队列模型

状态位 state 表示共享资源的同步状态,所有的线程都会争抢 state,AQS 中把所有的竞争线程组织成同步双向队列模型,结构如下:

在这里插入图片描述

head 和 tail 分别是 AQS 中的变量,其中 head 指向同步队列的头部,注意 head 为空结点,不存储信息;而 tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作

state 变量则是代表同步状态,执行当线程调用 lock 方法进行加锁后,如果此时 state 的值为 0,则说明当前线程可以获取到锁,同时将 state 设置为 1,表示获取成功;如果 state 已为 1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为 Node 结点加入同步队列等待

其中 Node 结点是对每一个访问同步代码的线程的封装,从图中的 Node 的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类,其内部实现我们在下面会详细了解

总结一下其中关键点如下:

  • state:用 volatile 修饰,保证修改能够在所有的线程间可见
  • Node结构: 节点中封装了线程,使所有未成功竞争到 state 的阻塞线程按照双向队列组织
  • 入队列、出队列:入队列和出队列用 CAS 技术,保证线程安全且避免使用互斥锁

二、状态位 state

使用锁的原因就在于存在共享资源,且存在竞争。state 就表示共享资源的状态,对于不同的同步工具类,state 具有各自独特的意义

state 是一个用 volatile 修饰的 int 类型的数据:

/**
 * The synchronization state.
*/
private volatile int state;

该 state 可以表示锁处于什么状态,其具体含义由子类实现来定义,比如:

  • ReentrantLock 用它来表示当前线程是否已经获得了锁(0代表未获得锁,1代表获得锁,3表示当前线程重入了3次)
  • Semaphore 用它来表现信号量剩余的许可数
  • CountDownLatch 用它来表示还需要 CountDown 多少次,门闩才能放开向下运行
  • FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)

使用AQS来实现一个同步器需要覆盖实现这几个方法,并且使用getState,setState,compareAndSetState这几个方法来设置获取共享资源的状态

三、Node 节点

AQS 的另一个核心就在于其把对于争夺锁资源的所有线程,在内部维护成一个先入先出的双向链表,结构如下

在这里插入图片描述

双向链表中的元素都是在 AQS 中定义的一个内部类 Node,其源码中的主要内容如下:

static final class Node {
    
    
    //共享模式
    static final Node SHARED = new Node();
    //独占模式
    static final Node EXCLUSIVE = null;

    //标识线程已处于结束状态
    static final int CANCELLED =  1;
    //等待被唤醒状态
    static final int SIGNAL    = -1;
    //条件状态,
    static final int CONDITION = -2;
    //在共享模式中使用表示获得的同步状态会被传播
    static final int PROPAGATE = -3;

    //等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种
    volatile int waitStatus;

    //同步队列中前驱结点
    volatile Node prev;

    //同步队列中后继结点
    volatile Node next;

    //请求锁的线程
    volatile Thread thread;

    //等待队列中的后继结点,这个与Condition有关,稍后会分析
    Node nextWaiter;

    //判断是否为共享模式
    final boolean isShared() {
    
    
        return nextWaiter == SHARED;
    }

    //获取前驱结点
    final Node predecessor() throws NullPointerException {
    
    
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    //.....
}

其中 SHARED 和 EXCLUSIVE 常量分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操作,如信号量 Semaphore 采用的就是基于 AQS 的共享模式实现的;而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如 ReentranLock 。变量 waitStatus 则表示当前被封装成 Node 结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE:

  • CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
  • CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
  • 0状态:值为0,代表初始化状态。

通过 Node 的源码我们可以发现关键在于每一个节点中的关键属性如下:

  • volatile Node prev:当前节点的前驱节点
  • volatile Node next:当前节点的后继节点
  • volatile Thread thread:封装了请求锁的线程

由节点的内部元素有指向前面和后面元素的指针,可以侧面验证节点是按照双向链表组织的,同时每一个节点封装一个 thread 元素,对应尝试获得共享资源 state 的线程

总结

AQS 作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于 AQS 实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成 Node 结点并将当前线程必要的信息存储到node结点中,然后加入同步队列等会儿获取锁,而这系列操作都有 AQS 协助我们完成,这也是作为基础组件的原因,无论是 Semaphore 还是 ReetrantLock,其内部绝大多数方法都是间接调用 AQS 完成的

四、出入同步队列的实现–CAS

AQS的实现主要在于维护一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。队列中的每个节点是对线程的一个封装,包含线程基本信息,状态,等待的资源类型等

每一个节点会首先会尝试通过 acquire() 方法获取锁,如果能够正常获取,说明当前线程获得锁;如果不能获取,说明锁被占用,就会调用 addWaiter() 方法,进入等待队列,这个操作是通过 CAS 操作来保证进出队列是线程安全的;当锁资源被释放后,会调用 acquireQueued() 方法,从clh中选一个线程获取占用共享资源 state

获取资源 state 的大致流程代码如下:

public final void acquire(int arg) {
    
    
        // 首先尝试获取,不成功的话则将其加入到等待队列,再for循环获取
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    // 从clh中选一个线程获取占用资源
    final boolean acquireQueued(final Node node, int arg) {
    
    
        boolean failed = true;
        try {
    
    
            boolean interrupted = false;
            for (;;) {
    
    
                // 当节点的先驱是head的时候,就可以尝试获取占用资源了tryAcquire
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
    
    
                    // 如果获取到资源,则将当前节点设置为头节点head
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果获取失败的话,判断是否可以休息,可以的话就进入waiting状态,直到被unpark()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
    }
  
   private Node addWaiter(Node mode) {
    
    
        // 封装当前线程和模式为新的节点,并将其加入到队列中
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
    
    
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
    
    
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }  
    
    private Node enq(final Node node) {
    
    
        for (;;) {
    
    
            Node t = tail;
            if (t == null) {
    
     
                // tail为null,说明还没初始化,此时需进行初始化工作
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
    
    
                // 否则的话,将当前线程节点作为tail节点加入到CLH中去
                node.prev = t;
                if (compareAndSetTail(t, node)) {
    
    
                    t.next = node;
                    return t;
                }
            }
        }
    }

上面只是获取共享资源 state 的大致的调用链,其中重点理解调用 addWriter() 方法使当前线程封装成节点,加入等待队列 CLH 的时候,是使用 CAS 来保证并发从尾部插入队列是线程安全的,对于不同的实现子类,又有不同的实现细节:

  • 共享资源 state 的状态又分为独占状态(代表是ReentrantLock) 和 共享状态(代表是 CountDownLatch 和 CyclicBarrier)
  • 获取锁的过程又分为公平和非公平的过程,其具体实现代表是 ReentrantLock

下面我们来研究一下这些具体的子类实现

五、基于CountDownLatch分析AQS共享模式的实现

下面以 CountDownLatch 举例说明基于 AQS 中共享模式的实现过程

CountDownLatch 即门闩的意思,门闩不打开,所有的线程都会阻塞不前,它用 AQS 的 state 来表示当前计数,其判断流程如下:

  • countDown 方法调用 release 从而导致计数器 state 递减,当计数器为0时,解除所有线程的等待
  • await调用 acquire,如果计数器为 0,acquire 会立即返回,门闩放开线程向下运行,否则阻塞

CountDownLatch 通常用于某任务需要等待其他任务都完成后才能继续执行的情景。源码如下:

public class CountDownLatch {
    
    
    /**
     * 基于AQS的内部Sync
     * 使用AQS的state来表示计数count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
    
    
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
    
    
            // 使用AQS的getState()方法设置状态
            setState(count);
        }

        int getCount() {
    
    
            // 使用AQS的getState()方法获取状态
            return getState();
        }

        // 覆盖在共享模式下尝试获取锁
        protected int tryAcquireShared(int acquires) {
    
    
            // 这里用状态state是否为0来表示是否成功,为0的时候可以获取到返回1,否则不可以返回-1
            return (getState() == 0) ? 1 : -1;
        }

        // 覆盖在共享模式下尝试释放锁
        protected boolean tryReleaseShared(int releases) {
    
    
            // 在for循环中Decrement count直至成功;
            // 当状态值即count为0的时候,返回false表示 signal when transition to zero
            for (;;) {
    
    
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    // 使用给定计数值构造CountDownLatch
    public CountDownLatch(int count) {
    
    
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    // 让当前线程阻塞直到计数count变为0,或者线程被中断
    public void await() throws InterruptedException {
    
    
        sync.acquireSharedInterruptibly(1);
    }

    // 阻塞当前线程,除非count变为0或者等待了timeout的时间。当count变为0时,返回true
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    // count递减
    public void countDown() {
    
    
        sync.releaseShared(1);
    }

    // 获取当前count值
    public long getCount() {
    
    
        return sync.getCount();
    }

    public String toString() {
    
    
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

六、基于ReentrantLock分析AQS独占模式的实现

1、ReentrantLock 与 AQS 的关系

在这里插入图片描述

可以发现 ReentrantLock 内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中 Sync 继承自 AQS, 实现了解锁 tryRelease() 方法,而 NonfairSync(非公平锁)、 FairSync(公平锁) 则继承自 Sync,实现了获取锁的 tryAcquire() 方法,ReentrantLock 的所有方法调用都通过间接调用 AQS 和 Sync 类及其子类来完成的

从上述类图可以看出 AQS 是一个抽象类,但请注意其源码中并没一个抽象的方法,这是因为 AQS 只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等,事实上,从设计模式角度来看,AQS 采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,为什么这么做?

这是因为 AQS 作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用。也就是说:

  • 独占锁:如 ReentrantLock 需要自己实现 tryAcquire() 方法和 tryRelease() 方法,
  • 共享锁:如Semaphore 和 CountDownLatch,则需要实现 tryAcquireShared() 方法和 tryReleaseShared() 方法

这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可

AQS提供给独占模式和共享模式的模板方法如下:

//AQS中提供的主要模板方法,由子类实现。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    
    

    //独占模式下获取锁的方法
    protected boolean tryAcquire(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //独占模式下解锁的方法
    protected boolean tryRelease(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //共享模式下获取锁的方法
    protected int tryAcquireShared(int arg) {
    
    
        throw new UnsupportedOperationException();
    }

    //共享模式下解锁的方法
    protected boolean tryReleaseShared(int arg) {
    
    
        throw new UnsupportedOperationException();
    }
    //判断是否为持有独占锁
    protected boolean isHeldExclusively() {
    
    
        throw new UnsupportedOperationException();
    }

}

2、ReentrantLock非公平锁的实现

公平与非公平的判断:

  • 如果新线程来到之后二话不说就和同步队列中的头节点抢夺同步状态 state,那就是非公平锁
  • 如果新线程来到之后老老实实进 CLH 队列排队,那就是公平锁

AQS同步器的实现依赖于内部的同步队列 (FIFO的双向链表对列) 完成对同步状态 (state) 的管理,当前线程获取锁(同步状态)失败时,AQS 会将该线程以及相关等待信息包装成一个节点 (Node) 并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会将头结点head中的线程唤醒,让其尝试获取同步状态

关于同步队列 CLH 和 Node 结点,前面我们已进行了较为详细的分析,这里重点分析一下获取同步状态和释放同步状态以及如何加入队列的具体操作,这里从 ReetrantLock 入手分析 AQS 的具体实现,我们先以非公平锁为例进行分析

//默认构造,创建非公平锁NonfairSync
public ReentrantLock() {
    
    
    sync = new NonfairSync();
}
//根据传入参数创建锁类型
public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

//加锁操作
public void lock() {
    
    
     sync.lock();
}

前面说过sync是个抽象类,存在两个不同的实现子类,这里从非公平锁入手,看看其实现:

/**
 * 非公平锁实现
 */
static final class NonfairSync extends Sync {
    
    
    //加锁
    final void lock() {
    
    
        //执行CAS操作,获取同步状态
        if (compareAndSetState(0, 1))
       //成功则将独占锁线程设置为当前线程  
          setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则再次请求同步状态
            acquire(1);
    }
}

这里获取锁时,首先对同步状态执行 CAS 操作,尝试把 state 的状态从 0 设置为 1,如果返回 true 则代表获取同步状态成功,也就是当前线程获取锁成,可操作临界资源;如果返回 false,则表示已有线程持有该同步状态(其值为1),获取锁失败

注意这里存在并发的情景,也就是可能同时存在多个线程设置 state 变量,因此是 CAS 操作保证了 state 变量操作的原子性。返回 false 后,执行 acquire(1)方法,该方法是 AQS 中的方法,它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下:

public final void acquire(int arg) {
    
    
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里传入参数 arg 表示要获取同步状态后设置的值(即要设置 state 的值),因为要获取锁,而 status 为 0 时是释放锁,1 则是获取锁,所以这里一般传递参数为 1,进入方法后首先会执行 tryAcquire(arg) 方法,在前面分析过该方法在 AQS 中并没有具体实现,而是交由子类实现,因此该方法是由 ReetrantLock 类内部实现的:

//NonfairSync类
static final class NonfairSync extends Sync {
    
    

    protected final boolean tryAcquire(int acquires) {
    
    
         return nonfairTryAcquire(acquires);
     }
 }

//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
    
    

  //nonfairTryAcquire方法
  final boolean nonfairTryAcquire(int acquires) {
    
    
      final Thread current = Thread.currentThread();
      int c = getState();
      //判断同步状态是否为0,并尝试再次获取同步状态
      if (c == 0) {
    
    
          //执行CAS操作
          if (compareAndSetState(0, acquires)) {
    
    
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      //如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
      else if (current == getExclusiveOwnerThread()) {
    
    
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          //设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
          setState(nextc);
          return true;
      }
      return false;
  }
  //省略其他代码
}

从代码执行流程可以看出,这里做了两件事

  • 一是尝试再次获取同步状态,如果获取成功则将当前线程设置为 OwnerThread,否则失败
  • 二是判断当前线程 currentThread 是否为 OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false

需要注意的是 nonfairTryAcquire(int acquires) 内部使用的是 CAS 原子性操作设置 state 值,可以保证state 的更改是线程安全的,因此只要任意一个线程调用 nonfairTryAcquire(int acquires) 方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是 head 结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁,这点跟后面还会讲到的公平锁不同。ok~,接着看之前的方法 acquire(int arg)

public final void acquire(int arg) {
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果 tryAcquire(arg) 返回 true,这是最理想的,表示当前线程已获取到锁,acquireQueued 自然不会执行;如果 tryAcquire(arg) 返回 false,则会执行 addWaiter(Node.EXCLUSIVE) 进行入队列操作,由于 ReentrantLock 属于独占锁,因此结点类型为 Node.EXCLUSIVE ,下面看看 addWaiter() 方法具体实现:

private Node addWaiter(Node mode) {
    
    
    //将请求同步状态失败的线程封装成结点
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    //如果是第一个结点加入肯定为空,跳过。
    //如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
    if (pred != null) {
    
    
        node.prev = pred;
        //使用CAS执行尾部结点替换,尝试在尾部快速添加
        if (compareAndSetTail(pred, node)) {
    
    
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作没有成功执行enq入队操作
    enq(node);
    return node;
}

创建了一个 Node.EXCLUSIVE 类型 Node 结点用于封装线程及其相关信息,其中 tail 是 AQS 的成员变量,指向队尾(这点前面的我们分析过 AQS 维持的是一个双向链表同步队列),如果是第一个结点,则 tail 肯定为空,那么将执行 enq(node) 操作,如果不是第一个结点,即 tail 指向不指向 null,就直接尝试执行 CAS 操作加入队尾,如果CAS 操作失败还是会执行 enq(node),继续看 enq(node)

private Node enq(final Node node) {
    
    
    //死循环
    for (;;) {
    
    
         Node t = tail;
         //如果队列为null,即没有头结点
         if (t == null) {
    
     // Must initialize
             //创建并使用CAS设置头结点
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {
    
    //队尾添加新结点
             node.prev = t;
             if (compareAndSetTail(t, node)) {
    
    
                 t.next = node;
                 return t;
             }
         }
     }
}

这个方法使用一个死循环进行CAS操作,可以解决多线程并发问题

这里做了两件事:

  • 一是如果还没有初始同步队列则创建新结点并使用 compareAndSetHead 设置头结点,tail 也指向 head;
  • 二是队列已存在,则将新结点 node 添加到队尾

注意这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改 head 和 tail 成功,那么其他线程将继续循环,直到修改成功,这里使用 CAS 原子操作进行头结点设置和尾结点 tail 替换可以保证线程安全,从这里也可以看出 head 结点本身不存在任何数据,它只是作为一个牵头结点,而 tail 永远指向尾部结点(前提是队列不为null)

在这里插入图片描述

添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态 state,然后从同步队列退出并结束自旋,回到之前的 acquire() 方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的,代码如下:

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        //自旋,死循环
        for (;;) {
    
    
            //获取前驱结点
            final Node p = node.predecessor();
            //当且仅当p为头结点才尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
    
    
                //将node设置为头结点
                setHead(node);
                //清空原来头结点的引用便于GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驱结点不是head,判断是否挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            //最终都没能获取同步状态,结束该线程的请求
            cancelAcquire(node);
    }
}

当前线程在自旋(死循环)中获取同步状态,当且仅当前驱结点为头结点才尝试获取同步状态,这符合 FIFO 的规则,即先进先出,其次 head 是当前获取同步状态的线程结点,只有当 head 释放同步状态 state 唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为 head 时,才进行尝试获取同步状态,其他时刻将被挂起。进入if语句后调用setHead(node)方法,将当前线程结点设置为head:

//设置为头结点
private void setHead(Node node) {
    
    
        head = node;
        //清空结点数据
        node.thread = null;
        node.prev = null;
}

当前 node 结点被设置为 head 后,其 thread 信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head 只保存指向后继结点的指针即可,便于 head 结点释放同步状态后唤醒后继结点,执行结果如下图:

在这里插入图片描述

从图可知更新 head 结点的指向,将后继结点的线程唤醒并获取同步状态 state,调用 setHead(node) 将其替换为 head 结点,清除相关无用数据

关于获取锁的操作,这里看看另外一种可中断的获取方式,即调用 ReentrantLock 类的 lockInterruptibly() 或者 tryLock() 方法,最终它们都间接调用到 doAcquireInterruptibly()

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
    
    
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
    
    
            for (;;) {
    
    
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
    
    
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //直接抛异常,中断线程的同步状态请求
                    throw new InterruptedException();
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
    }

最大的不同是:

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
     //直接抛异常,中断线程的同步状态请求
       throw new InterruptedException();

检测到线程的中断操作后,直接抛出异常,从而中断线程的同步状态请求,移除同步队列,加锁流程到此结束。下面接着看unlock()操作:

//ReentrantLock类的unlock
public void unlock() {
    
    
    sync.release(1);
}

//AQS类的release()方法
public final boolean release(int arg) {
    
    
    //尝试释放锁
    if (tryRelease(arg)) {
    
    

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继结点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock类中的内部类Sync实现的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {
    
    

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判断状态是否为0,如果是则说明已释放同步状态
      if (c == 0) {
    
    
          free = true;
          //设置Owner为null
          setExclusiveOwnerThread(null);
      }
      //设置更新同步状态
      setState(c);
      return free;
  }

释放同步状态的操作相对简单些,tryRelease(int releases)方法是 ReentrantLock 类中内部类自己实现的,因为 AQS 对于释放锁并没有提供具体实现,必须由子类自己实现。释放同步状态后会使用 unparkSuccessor(h) 唤醒后继结点的线程,这里看看 unparkSuccessor(h)

private void unparkSuccessor(Node node) {
    
    
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {
    
    //如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

从代码执行操作来看,这里主要作用是用 unpark() 唤醒同步队列中最前边未放弃线程(也就是状态为 CANCELLED的线程结点 s)。此时,回忆前面分析进入自旋的函数 acquireQueued(),s 结点的线程被唤醒后,会进入acquireQueued()函数的 if (p == head && tryAcquire(arg)) 的判断,如果 p!=head 也不会有影响,因为它会执行 shouldParkAfterFailedAcquire(),由于 s 通过 unparkSuccessor() 操作后已是同步队列中最前边未放弃的线程结点,那么通过 shouldParkAfterFailedAcquire() 内部对结点状态的调整,s 也必然会成为 head的 next 结点,因此再次自旋时 p==head 就成立了,然后 s 把自己设置成 head 结点,表示自己已经获取到资源了,最终 acquire() 也返回了,这就是独占锁释放的过程

关于独占模式的加锁和释放锁的过程到这就分析完,总之在AQS同步器中维护着一个同步队列,当线程获取同步状态失败后,将会被封装成Node结点,加入到同步队列中并进行自旋操作,当前线程结点的前驱结点为head时,将尝试获取同步状态,获取成功将自己设置为head结点。在释放同步状态时,则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态,释放成功则唤醒后继结点的线程

3、ReentrantLock非公平锁的实现

了解完 ReetrantLock 中非公平锁的实现后,我们再来看看公平锁

与非公平锁不同的是,在获取锁的时,公平锁的获取顺序是完全遵循时间上的 FIFO 规则,也就是说先请求的线程一定会先获取锁,后来的线程肯定需要排队,这点与前面我们分析非公平锁的 nonfairTryAcquire(int acquires) 方法实现有锁不同,下面是公平锁中 tryAcquire() 方法的实现:

//公平锁FairSync类中的实现
protected final boolean tryAcquire(int acquires) {
    
    
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
    
    
            //注意!!这里先判断同步队列是否存在结点
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
    
    
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
    
    
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

该方法与 nonfairTryAcquire(int acquires) 方法唯一的不同是在使用 CAS 设置尝试设置 state 值前,调用了 hasQueuedPredecessors() 判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态

这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态 state,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁,所以 ReentrantLock 默认调用的构造方法是非公平锁

以上便是 ReentrantLock 的内部实现原理,这里我们简单进行小结:

重入锁 ReentrantLock 是一个基于 AQS 并发框架的并发控制类,其内部实现了3个类,分别是Sync、NoFairSync 以及 FairSync 类,其中 Sync 继承自 AQS,实现了释放锁的模板方法 tryRelease(int),而NoFairSync 和 FairSyn c都继承自Sync,实现各自获取锁的方法 tryAcquire(int)

ReentrantLock 的所有方法实现几乎都间接调用了这3个类,因此当我们在使用 ReentrantLock 时,大部分使用都是在间接调用 AQS 同步器中的方法

以上就是 ReentrantLock 的所有内部实现原理,最后给出 Reentrant 的类图结构

在这里插入图片描述

参考:《Java并发编程实战》

参考文章:

https://blog.csdn.net/javazejian/article/details/75043422

多线程—Java内存模型与线程

多线程——Volatile 关键字详解

多线程——线程安全及实现机制

多线程——深入剖析 Synchronized

多线程\并发编程——ReentrantLock 详解

多线程/并发编程——CAS、Unsafe及Atomic

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/108746299
今日推荐