AQS 原理和 ReentrantLock 源码

674 阅读6分钟

说明

  1. 本文中采用的 jdk 版本为 openjdk-1.8

锁和线程状修改

  1. 加锁的本质?主要是为了在访问临界资源的时候,能够实现一个等待唤醒得有序操作。
  2. Java 中的锁的分类: sychronizedLock 。在 sychronized 中主要是通过 monitor 来进行实现的。通过 Object.wart/notify 实现线程得阻塞和唤醒; 第二种就是基于线程的 LockSupport.park/unpack 阻塞和唤醒。
  3. 线程的中断问题,如何优雅的中断一个线程?
  • Java中断机制是一种协作机制,也就是说中断并不能直接终止某一个线程,而需要被中断的线程自己处理中断

  • API 的使用:

    interrupt(): 将线程的中断标示位设置为 true;

    isInterrupted(): 判断当前线程的中断标志位是否是 true;

    Thread.interrupted(): 判断当前线程中断位置是否位 true, 并且清除中断标志位,重置为 fasle。

  1. LockSupport 会造成线程中断吗? LockSupport 不会造成线程中断的。

  2. CAS 是什么?比较交换, 主要是一个乐观锁的概念, 底层采用的是 unsafe api 来实现比较交换。

protected final boolean compareAndSetState(int expect, int update) {    
    // See below for intrinsics setup to support this    
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 的原理和实现

AQS 是基于队列在实现排队 AbstractQueuedSynchronizer 类为模板方法的实现。

  1. 底层数据结构是一个双向链表。每个 Node 节点包含 prev,和 next 指针,以及数据数据字段,这里的数据字段用就是一个线程 Thread 对象。

  2. Node 的四种状态: 取消,等待,条件等待,共享状态

  3. 通常的两种实现:公平锁,非公平锁。 怎么提现公平指的是同一个时刻新加入的数据,和队列头的数据竞争是否能够进行公平的资源竞争。

  4. 状态的修改通过 CAS 来实现,底层是调用 sun.misc.Unsafe 的 compareAndSwapInt 进行状态的修改。

  5. 在线程进入队列之前会进行尝试加锁,如果拿不到锁会阻塞当前线程并且线程通过 LockSupport.park() 进入阻塞。

  6. 释放锁的时候,就会去队列中拿队列头的节点,进行唤醒,同步队列的头节点 head 就是当前获取锁的线程

  7. 下面是获取锁中 AQS 的核心代码

    a. 尝试加锁,如果加锁不成功,就进入队列进行重试。

     // 独占的方式获取锁, 可以忽略中断, 最少调用一次,如果失败会进行排队,直到成功
     public final void acquire(int arg) {
         if (!tryAcquire(arg) &&
             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
             selfInterrupt();
     }
    

    b. 队列中,先去判断是否是队列头,如果是去尝试加锁,如果不是就对 Node 的状态进行修改,修改为等待唤醒。状态修改成功后把线程状态修改为阻塞。

    	final boolean acquireQueued(final Node node, int arg) {
         boolean failed = true;
         try {
             boolean interrupted = false;
             for (;;) {
                 final Node p = node.predecessor();
                 if (p == head && tryAcquire(arg)) {
                     setHead(node);
                     p.next = null; // help GC
                     failed = false;
                     return interrupted;
                 }
                 if (shouldParkAfterFailedAcquire(p, node) &&
                     parkAndCheckInterrupt())
                     interrupted = true;
             }
         } finally {
             if (failed)
                 cancelAcquire(node);
         }
     }
    

    下面是 shouldParkAfterFailedAcquire 方法对 Node 的状态进行维护。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
         int ws = pred.waitStatus; // 前继节点的状态, 第一次进入默认值 0
         if (ws == Node.SIGNAL)
             return true;
         if (ws > 0) {
             do {
                 // 出队
                 node.prev = pred = pred.prev;
             } while (pred.waitStatus > 0);
             pred.next = node;
         } else {
             // 第一次进来, pred.waitStatus = 0 执行这个分支
             // 将前继节点的状态修改为 SIGNAL, 表示 pred.next 节点需要被唤醒(此时准备进入阻塞, 但是还未被阻塞, 再次获取锁失败之后才会被阻塞)
             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
         }
         return false;
     }
    

AQS 流程图

ASQ 特征

  1. 阻塞等待队列
  2. 共享独占
  3. 公平/非公平
  4. 可重入
  5. 允许中断

ReentrantLock

一个简单的 Demo

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    static ReentrantLock lock = new ReentrantLock();

    static class T extends Thread {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread() + "开始尝试获取锁");
                if (lock.tryLock(10, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread() + "成功获取锁");
                    TimeUnit.SECONDS.sleep(5);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread() + "开始释放锁");
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
    }
}

加锁和解锁过程图解

加锁和解锁过程描述

  1. 在上面的程序中有三个线程同时去获取锁,同一个时刻只能又一个线程获取到锁,下面是进行入队去尝试获取锁的逻辑:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
  if (nanosTimeout <= 0L)
      return false;
  final long deadline = System.nanoTime() + nanosTimeout;
  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 true;
          }
          // 获取锁超过最大等待时间
          nanosTimeout = deadline - System.nanoTime();
          if (nanosTimeout <= 0L)
              return false;
          // 获取锁失败进入阻塞 并且 并且超过自旋等待时间
          if (shouldParkAfterFailedAcquire(p, node) &&
              nanosTimeout > spinForTimeoutThreshold)
              // 进入阻塞 nanosTimeout 为阻塞时间
              LockSupport.parkNanos(this, nanosTimeout);
          if (Thread.interrupted())
              throw new InterruptedException();
      }
  } finally {
      if (failed)
          cancelAcquire(node);
  }
}
  1. 非公平锁尝试加锁的逻辑, 如果没有线程持有锁,那么就去通过 CAS 尝试加锁,如果是当前线程持有锁那么就 state + 1 累计,这里也可以看出 ReentrantLock 支持重入。
// 非公平锁的逻辑
// 如何理解插队, 这里的插队是当前队列中被唤醒的线程, 和当前加入的线程都可以被执行
// 如果当前加入线程比队列中唤醒的线程先获取到锁, 就是插队现象
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 无锁状态, 尝试竞争
    if (c == 0) {
        if (compareAndSetState(0, acquires)) { //是否获取到锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 当前线程持有锁, state 计数 +1
    else if (current == getExclusiveOwnerThread()) { //判断是否是重入
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  1. 如果获取到锁, 会将当前节点设置为头节点。并且返回 true 如果获取锁失败,并且超过获取锁的自旋时间那么当前线程将进入阻塞,阻塞是通过调用 LockSupport.parkNanos(this, nanosTimeout);实现的。在这个过程中可能调用多次 shouldParkAfterFailedAcquire 方法。shouldParkAfterFailedAcquire 可以用来修改当前节点的状态,和对链表上无效的节点出队
/** 当获取锁失败后, 检查更新新节点状态如果是需要阻塞返回, true
 * <p>
 * 一个前继节点 waitStatus = 0, 第一次将继续设置为 SIGNAL, 告诉当前线程准备进入阻塞, 此时依旧获取不到, 当前线程进入阻塞
 *
 * @param pred 前继节点
 * @param node 当前节点
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 前继节点的状态, 第一次进入的话, 一定是 0
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            // 出队, 剔除无效的节点
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 第一次进来, pred.waitStatus = 0 执行这个分支
        // 将前继节点的状态修改为 SIGNAL, 表示 pred.next 节点需要被唤醒(此时准备进入阻塞, 但是还未被阻塞, 再次获取锁失败之后才会被阻塞)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 解锁逻辑,下面是解锁的逻辑, 首先会进行解锁,如果 state 的状态修改为 0, 然后再去唤醒队列中排队的线程。
// 解锁
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // 判断是否有需要唤醒的线程
        if (h != null && h.waitStatus != 0) //waitStatus 的值为 0, 只有当后继存在节点才会被设置为该值不为 0, 此时需要唤醒后继线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// tryRelease 
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 判断是否是当前线程持有锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
    	// 如果 state == 0 表示当前线程不在占有该锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

// 唤醒队列中的线程
private void unparkSuccessor(Node node) {
	// 将当前节点状态修改为 0  
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
    	// 唤醒队列中的节点
        LockSupport.unpark(s.thread);
}
  1. 当前节点被唤醒逻辑,首先会在 shouldParkAfterFailedAcquire 方法中出队,然后尝试加锁如果加锁成功就返回 true.