【Java 多线程】5 - 8 深入了解可重入锁

§5-8 深入了解可重入锁

5-8.1 ReentrantLock 可重入锁类

下文内容参考自:

这一次,彻底搞懂Java中的ReentrantLockt实现原理 - 掘金 (juejin.cn)

在线程安全与线程锁一节中,同时介绍了监视器锁和 ReentrantLock 的使用方法,用于解决线程安全问题。其中,监视器锁依赖于 JVM 实现,获取锁与释放锁操作自动进行。而 ReentrantLock 则提供了一种更为灵活的手动上锁解锁方式。语义上,ReentrantLock 与对象监视器锁相同。二者都是可重入的锁。

使用 ReentrantLock 时,一般结合 try-finally 代码块使用,且 unlock() 方法调用位于 finally 代码块中,避免死锁。

先来回顾一下 ReentrantLock构造方法

构造方法 描述
ReentrantLock() 创建一个默认为非公平的可重入锁
ReentrantLock(boolean fair) 创建一个指定公平性政策的可重入锁

这里就涉及到公平锁与非公平锁的概念。

  • 公平锁:公平锁会将锁优先让给等待时间最长的线程。这可以避免线程长时间无法获取锁的情况,但整体吞吐率会有所降低,且等待队列中除持有者线程外其他线程都会被阻塞,CPU 唤醒线程的开销较大;
  • 非公平锁:非公平锁允许线程不按照锁的申请顺序获取锁,每个线程在尝试加锁时都会尝试获取锁。若成功获取,则线程无需等待,直接执行。这样可保证整体吞吐率,效率较高,且可以避免线程唤醒带来的资源浪费。但这可能会导致有线程始终无法获取锁,处于 “饥饿” 状态;

此外,也应该了解可重入与非可重入锁的概念。

  • 可重入锁:又称递归锁,同一个线程再次获取同一把锁时,线程会自动获取锁(前提为锁对象为同一个对象或 class),而不会因为先前获取的锁未释放而阻塞;
  • 非可重入锁:与可重入锁是对立关系。不允许同一条线程反复获取同一把锁,即使该锁是同一个对象;

对象监视器锁和 ReentrantLock 都是可重入锁。

另外,注意到在官方 API 文档中,对 ReentrantLock 的介绍中提及到,ReentrantLock 是一个互斥锁。这里需要明确一下互斥锁与共享锁的概念。

  • 互斥锁(mutual exclusion lock):也称排他锁,该锁一次只能由一个线程持有,且对由锁对象所保护的内容不具有读写权限,只有持有者线程才具备读取和修改的权限。若线程对数据加上排他锁后,其他线程就无法再对该数据加上任何类型的锁;
  • 共享锁(shared lock):共享锁可由多个线程同时持有,线程获取共享锁后,只能够读取数据,不能修改。线程对数据加上共享锁后,其他线程只能对该数据添加共享锁,不能添加排他锁;

ReentrantLock 的锁采用的是互斥锁。

考虑到 ReentrantLock 具有公平锁和非公平锁之分,现在从代码层面上看看 ReentrantLock 实例的运作过程。

5-8.2 构造器与 Lock 的方法

ReentrantLock 的类签名为:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    // 其他成员变量、内部类、方法等
}

ReentrantLock 实现了 Lock 接口,那么必定要重写其中的方法。

ReentrantLock 重写了 Lock 的方法:

// 重写自Lock接口的方法
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync;
    
    public void lock() {
        sync.lock();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.lockInterruptibly();
    }

    public boolean tryLock() {
        return sync.tryLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryLockNanos(unit.toNanos(timeout));
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }
}

现在再来看看构造器的源码:

// 默认无参构造器
public ReentrantLock() {
    // 为成员变量 sync 赋值,创建一个非公平同步器
    sync = new NonfairSync();
}

// 有参构造器,参数决定锁实例是否为公平锁
public ReentrantLock(boolean fair) {
    // 使用三目运算符,决定将成员变量指向哪一种同步器实例
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到,构造器中只是对成员变量 sync 进行 “赋值”,根据实际情况决定创建何种同步器。接下来,跟踪该变量的定义,得到:

// 提供所有实现机制的同步器
private final Sync sync;

从注释可得知,ReentrantLock 中的所有与锁有关方法(同步实现机制)都与这个变量有关。该变量的类型是一个位于 ReentrantLock 的一个抽象内部类,这个类在稍后会讲。

可以看到,所有与锁操作有关的方法,在内部都交由 sync 完成。接下来我们来看看 sync 的定义。

5-8.3 Sync 类及其子类

sync 变量定义于 ReentrantLock 中,其类型为 Sync。这是一个抽象内部类,其定义和部分方法如下所示:

// 该锁的同步控制基础。具有两个子类:公平与非公平版本。
// 使用AQS状态表示锁的持有数
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    
    // 根据公平性政策检查锁的可重入性,并在可用时立即获取锁
    // 该方法为抽象方法,取决于具体实现(由子类重写)
    abstract boolean initialTryLock();
    
    //获取锁
    final void lock() {
        // 线程两次获取锁,先调用initialTryLock初次尝试获取锁,失败后调用AQS的acquire尝试取锁
        if (!initialTryLock())	// 初次尝试获取锁
            acquire(1);			// 获取失败时再调用父类AQS的方法
    }
    
    // 非公平 tryLock
    final boolean tryLock() {
        Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // CAS 算法修改线程持有数
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (getExclusiveOwnerThread() == current) {
            if (++c < 0) // 溢出,超出可重入次数上限(Integer.MAX_VALUE)
                throw new Error("Maximum lock count exceeded");
            setState(c);
            return true;
        }
        return false;
    }
    
    // 可中断获取锁:在线程不中断的状态下获取锁,否则抛出异常
    final void lockInterruptibly() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!initialTryLock())
            acquireInterruptibly(1);
    }
    final boolean tryLockNanos(long nanos) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return initialTryLock() || tryAcquireNanos(1, nanos);
    }
    
    // 释放锁:重定义自AQS的尝试释放锁方法
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (getExclusiveOwnerThread() != Thread.currentThread())
            // 非持有者线程操作,抛出非法监视器异常
            throw new IllegalMonitorStateException();
        boolean free = (c == 0);	// 是否完全释放
        if (free)
            setExclusiveOwnerThread(null);
        setState(c);
        return free;
    }
    
    //锁状态相关方法
    // 获取锁的持有者
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
	// 锁的持有次数
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }
	// 是否上锁
    final boolean isLocked() {
        return getState() != 0;
    }
}

非公平锁实现类 NonfairSync

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 初次获取锁:线程尝试获取锁(重定义自Sync)
    final boolean initialTryLock() {
        Thread current = Thread.currentThread();	// 获取当前线程
        if (compareAndSetState(0, 1)) {
            // CAS 算法:若线程尝试获取锁
            setExclusiveOwnerThread(current);
            return true;
        } else if (getExclusiveOwnerThread() == current) {
            // 线程尝试重入获取锁
            int c = getState() + 1;
            if (c < 0) // 溢出:重入次数超出 Integer.MAX_VALUE
                throw new Error("Maximum lock count exceeded");
            setState(c);
            return true;
        } else
            // 取锁失败
            return false;
    }

    // initialTryLock 方法失败后的非重入获取方法(重定义自AQS)
    protected final boolean tryAcquire(int acquires) {
        // 在锁处于释放状态下采用CAS算法尝试让线程获取锁
        if (getState() == 0 && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

公平锁实现类 FairSync

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

   // 初次获取锁:仅当锁可重入或队列为空时获取锁
    final boolean initialTryLock() {
        Thread current = Thread.currentThread();	// 获取当前线程
        int c = getState();							// 锁同步状态
        if (c == 0) {								// 锁处于释放状态
            // 队列为空时采用CAS算法获取锁
            if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (getExclusiveOwnerThread() == current) {
            // 持有者线程尝试重入锁
            if (++c < 0) 							// 溢出:超出 Integer.MAX_VALUE
                throw new Error("Maximum lock count exceeded");
            setState(c);
            return true;
        }
        // 上述条件都不满足,队列不为空且并非进行重入尝试,则取锁失败
        return false;
    }

    // 仅当线程为第一个等待线程或队列为空时获取锁
    protected final boolean tryAcquire(int acquires) {
        // 锁处于释放状态,且为第一个等待线程时,用CAS算法尝试取锁
        if (getState() == 0 && !hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

公平锁与非公平锁的实现语义都体现在了其自身重定义的 tryAcquire 方法中,这也是真正获取锁时所依赖的方法。Sync 中对所操作的方法操作都比较简单。通过 getState()setState() 方法修改锁的获取和释放状态。值得注意的是,上面列出的源中,有部分方法的源码调用了其父类 AbstractQueuedSynchronizer 的方法。同样地,与锁的获取和释放的方法(lockunlock)也调用了其父类的有关方法,若要想详细了解 ReentrantLock,则必须要了解这个类。

5-8.4 AQS 和 AOS 类

Sync 直接继承自 AbstractQueuedSynchronizer 类,简称为 AQS。

AQS 简介

提供一个实现了阻塞锁和有关同步器的框架,依赖于一个先入先出的等待队列。该类作为多种同步器的实用基础,依赖一个单独的 int 原子变量表示锁状态。子类必须定义改变该状态的受保护方法,并且给出获取和释放对象的状态定义。类中的其他方法实现了入列和阻塞机制。子类可以维护其他状态字段,但只有使用 getState(), setState(int), compareAndSetState(int,int) 方法操作的原子更新的 int 值用于跟踪同步。

子类应当定义为一个实现其外围类同步属性的非公开的内部助手类。AQS 并不实现任何同步接口。相反,AQS 定义了诸如 acquireInterruptibly(int) 之类的方法,具体锁和相关同步器可在适当的时候调用这些方法以实现它们的公共方法。

该类支持独占模式和共享模式,二选一或二者兼备。线程以独占模式获取锁时,其他线程尝试获取锁时会失败。多个线程以共享模式获取锁时可能(但不需要)成功。该类并不 “明白” 模式的区别,仅在共享锁获取成功时,下一等待中线程(若有的话)必须也决定其是否也能够获取锁的情况下,锁明白模式的机制含义上的区别。在不同模式下等待的线程共享同一个先入先出队列。通常,实现子类只支持其中一种模式,但也可以同时支持两种模式(如 ReadWriteLock)。仅支持独占模式或共享模式的子类并不需要定义支持其不使用的模式的方法。

AQS 类定义了一个内部类 AbstractQueuedSynchronizer.ConditionObject,用作独占模式子类的 Condition 实现。isHeldExclusively() 方法返回同步是否由当前线程独占;release(int) 方法调用当前的 getState() 值完全释放该对象;acquire(AbstractQueuedSynchronizer.Node, int, boolean, boolean, boolean, long) 方法在给予所存状态值的情况下,最终会将该对象恢复至上一个取锁状态。AQS 方法不会创建这样的条件,所以若不能满足这一条件,则不要使用。AQS 的 ConditionObject 依赖于其同步器的实现语义。

该类为内部队列提供检查、检测和监视方法,以及条件对象的对应相似方法。可以按需使用一个 AQS 将这些方法导出到一个类中,以将方法的同步机制一并导出到类中。

该类的序列化只存储底层所依赖的原子整型所维护的值,因此反序列化后将会得到空线程队列。要求序列化能力的典型子类定义一个 readObject 方法,方法会将整型变量在反序列化时恢复成一个已知初始值。

AQS 用法

子类要实现锁的功能,以 AQS 为基础,应当重定义以下方法:

  • tryAcquire(int)
  • tryRelease(int)
  • tryAcquireShared(int)
  • tryReleaseShared(int)
  • isHeldExclusively()

为保证可用性,方法应当通过 getState(),setState(int)compareAndSetState(int,int) 检查并/或修改同步状态。

上述方法默认都只抛出异常 UnsupportedOperationException。这些方法的实现必须在内部线程安全,也应当总体上短小且不阻塞。定义这些方法是使用 AQS 的唯一方式。其他所有方法都声明为 final,用于实现同步锁的基本功能和语义,保证其不能够独立变化,从而保证锁功能的实现。

尽管该类基于一个内部的先入先出队列,但它并不自动强制执行先入先出取元素的政策。独占模式的同步核心具有以下形式:

// 获取:
while (!tryAcquire(arg)) {
    // 若并未入列,让线程入列;
    // 可能阻塞当前线程;
}
// 释放:
if (tryRelease(arg))
    // 反阻塞(unblock)第一个入列线程;

(共享模式也具有类似形式,但可能会调用级联信号)

现在来看看 AQS 的结构:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    private static final long serialVersionUID = 7373984972572414691L;

    private transient volatile Node head;	// 等待队列的头结点
    private transient volatile Node tail;	// 等待队列的尾结点
    private volatile int state;				// 同步状态
    
    // 使用 Unsafe 类的方法定位对象中字段所分配的内存地址
    // 用于 AQS 中 CAS 算法中的偏移量参数
    private static final long STATE
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "state");
    private static final long HEAD
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "head");
    private static final long TAIL
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "tail");
}

AQS 继承了 AbstractOwnableSynchronizer(AOS)。可以使用从 AOS 继承的实用方法,用于追踪持有独占同步器的线程。鼓励使用这些方法,这些监控和诊断工具帮助用户判定哪些线程持有锁。

AOS 的结构为:

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    private static final long serialVersionUID = 3737899427754241961L;
    
    // 供子类使用的构造器
    protected AbstractOwnableSynchronizer() { }
    
    // 排他锁的持有者线程
    private transient Thread exclusiveOwnerThread;
    
    // 设置持有者线程
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    
    // 获取持有者线程
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

综合来看,AOS 只是维护了锁的持有者线程,而 AQS 还维护了等待队列(存储封装了线程的 Node 结点对象,且队列是一个 CLH 队列)和锁同步状态(持有次数,为零表示未上锁,大于零表示已上锁)。

结点类将在稍后提到。

5-8.5 获取锁的过程

获取锁, 需要在同步区域开始处调用 ReentrantLocklock 方法即可。方法底层调用 sync.lock() 语句,该方法是 Sync 的一个 final 方法。其实现为一个 if 代码块,首先会调用 initialTryAcquire 方法尝试获取锁,这个方法取决于具体实现(公平锁或非公平锁),这个方法取锁失败时,才会调用分支当中的 acquire 方法再次尝试获取锁。

调用流程:

public class ReentrantLock implements Lock, java.io.Serializable {
    //...
    final void lock() {
        if (!initialTryLock())	//由具体实现类实现(FairSync 或 NonfairSync)
            acquire(1);			//AQS 的 final 方法
    }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    //...
    abstract initialTryLock();	//由子类实现(FairSync或NonfairSync)
}

static class FairSync extends Sync {
    
}

acquire 方法是 AQS 的 final 方法:

/**以独占模式、忽略中断、至少调用一次tryAcquire的方式获取锁,成功时返回
  *否则,线程将会进入队列中,可能会在阻塞和非阻塞的状态中反复切换,不断调用tryAcquire直至成功
  *该方法用于实现方法Lock.lock
  */
public final void acquire(int arg) {
    //独占模式锁的核心实现形式
    if (!tryAcquire(arg))	//调用运行时类的tryAcquire方法,尝试获取锁(例如:FairSync, NonfairSync)
        acquire(null, arg, false, false, false, 0L);
}

//AQS中的 tryAcquire 方法
protected boolean tryAcquire(int arg) {
    //只是抛出一个异常,要求子类在实现时重写该方法
    throw new UnsupportedOperationException();
}

可以看到 aquire 的方法内部选择分支又调用了一个 aquire 方法,该方法的签名和介绍为:

/**
  * 主 acquire 方法,由所有导出的 acquire 方法调用
  */
//在acquire(int)的调用:acquire(null, arg, false, false, false, 0L);
final int acquire(Node node,				//若非重获取Condition(reacquiring condition),则为null
                  int arg,					//取锁参数
                  boolean shared,			//true则为共享模式,false为独占模式
                  boolean interruptible,	//是否在线程中断时中止并负值
                  boolean timed, 			//是否限时等待
                 long time);				//若限时,超时时间单位为System.nanoTime()
//返回值:若成功取锁则返回正值,超时则返回0,线程中断则返回负值

/*
 * 重复执行:
 * 检查node现在是否为第一个结点
 * 若是第一个结点,则确保头结点稳定,否则确保其具有有效的前驱结点
 * 若结点是首结点或尚未入列,尝试获取锁
 * 否则若结点尚未创建,则创建结点
 * 否则若结点尚未入列,尝试入列
 * 否则若从挂载中唤醒,重试(直至postSpins次)
 * 否则若尚未设置等待,则设置状态并重试
 * 否则 park 并清除等待状态,并检查取消状态
 */

方法的形参中有 Node 对象,该类是 AQS 中的一个抽象类,用于封装等待的线程,存储到等待队列中。

public class AbstractQueuedSynchronizer 
    extends AbstractOwnableSynchonizer 
    implements java.io.Serializable {
    //...
    private transient volatile Node head;	//等待队列的头结点,弱初始化
    private transient volatile Node tail;	//等待队列的尾结点,初始化后,仅通过casTail方法修改
    
    //CLH 结点
    //CLH队列是Craig, Landin 和 Hagersten 三人的名字,这是一个单向链表
    abstract static class Node {
        volatile Node prev;       // 前驱结点,通过casTail链接
        volatile Node next;       // 后继节点,可发送信号时,则为非空
        Thread waiter;            // 等待线程,入列时为非空
        volatile int status;      // 结点状态参数,定义在AQS中,由持有者写入,由其他线程进行原子位操作

        //使用Unsafe类实例进行原子操作的方法(省略)
    }
    
    //与结点状态有关的状态参数
    //结点状态比特位,也用作参数和返回值
    static final int WAITING = 1;				// 表示等待中线程,值必须为1
    static final int CANCELLED = 0x80000000;	//表示线程被取消(队列等待超时或中断),必须为负值
    static final int COND = 2;					//线程处于条件等待
    
    //Node的具体类,通过类型标记
    static final ExclusiveNode extends Mode {}
    static final SharedNode extends Node {}
    static final ConditionNode extends Node 
        implements ForkJoinPool.ManagedBlocker {
        ConditionNode nextWaiter;		//链接到下一个等待结点
        
        /**
          * 允许Conditions在无固定池耗尽的风险条件下在ForkJoinPool(线程工作窃取池)中使用。
          * 这仅在无限时Condition等待中使用,而不是限时版本。
          */
        public final boolean isReleasable() {
            return status <= 1 || Thread.currentThread().isInterrupted();
        }
        
        public final boolean block() {
            while (!isReleasable()) LockSupport.park();
            return true;
        }
    }
}

至此,获取锁的过程已结束。

5-8.6 acquire 方法的实现

acquire(Node, int, boolean, boolean, boolean, long) 方法在 initialTryLock()tryAcquire(int) 取锁失败后,由 acquire(int) 调用。

该方法的实现为:

// 调用者传递参数:acquire(null, 1, false, false, false, 0L);
final int acquire(Node node, int arg, boolean shared,
                  boolean interruptible, boolean timed, long time) {
    
    Thread current = Thread.currentThread();	// 指向当前线程
    byte spins = 0, postSpins = 0;   			// 自旋,在第一个线程unpark(取消挂起)时重试操作
    boolean interrupted = false, first = false;	// 指示线程是否被打断、是否为第一个线程(结点)
    Node pred = null;                			// 入列时,结点的前驱结点,默认为空
    // 等待队列默认有一个头结点,但该头结点并不封装任何线程
    // 这些变量(除 current 外),都具有默认值

    // 循环,内含三个选择分支
    // 选择分支的布尔表达式嵌套了赋值语句,应当注意执行时机和执行可能性(考虑是否可能被短路)
    // 选择分支的布尔表达式一般情况下会从左到右逐个计算,先操作数的值,再计算部分表达式的值,如此往复
    for (;;) {
        // 条件:与表达式
        // 1. 不是首结点(队列第一个线程),该部分若为 false 则会短路该表达式的后续计算;
        // 2. 且结点前驱存在(若未被短路,则先计算三目运算符表达式的值),该部分若为 false 则会短路该表达式的后续计算;
        // 3. 前两个条件若都满足,则判断是否为队列的第一个结点(头结点的后继)
        if (!first && (pred = (node == null) ? null : node.prev) != null &&
            !(first = (head == pred))) {
            if (pred.status < 0) {
                cleanQueue();           // 前驱结点的线程被取消
                continue;
            } else if (pred.prev == null) {
                Thread.onSpinWait();    // 确保序列化
                continue;
            }
        }
        
        // 条件:若为首结点或前驱结点为空(两个变量条件满足其一)
        if (first || pred == null) {
            // 调用 tryAcquire 尝试获取锁,并用 acquired 变量标记是否成功获取锁
            boolean acquired;
            try {
                if (shared)		//共享模式
                    acquired = (tryAcquireShared(arg) >= 0);
                else			//独占模式,这里是由ReentrantLock.Sync调用,一定为独占模式(互斥锁)
                    acquired = tryAcquire(arg);
            } catch (Throwable ex) {
                // 捕获可能发生的异常,一旦出现异常则取消获取锁,并把异常抛出
                cancelAcquire(node, interrupted, false);
                throw ex;
            }
            //若成功获取锁,应当出列
            if (acquired) {
                // 若为首结点,则清除其前驱结点、等待线程,并让头结点指向该结点
                if (first) {
                    node.prev = null;
                    head = node;
                    pred.next = null;
                    node.waiter = null;
                    if (shared)
                        // 若为共享锁,其他线程还需要决定是否能够获取锁
                        // 但在这种情况下,ReentrantLock 是互斥锁,此处会跳过
                        signalNextIfShared(node);
                    if (interrupted)
                        current.interrupt();
                }
                //获取成功,返回
                return 1;
            }
        }
        
        // 若结点尚不存在
        if (node == null) {                 // 分配; 在入列前重试重试操作
            // 根据锁的模式创建对应的具体结点类对象
            if (shared)
                node = new SharedNode();	// 共享模式
            else
                node = new ExclusiveNode();	// 独占模式
        } else if (pred == null) {          // 尝试入列
            node.waiter = current;			// 结点封装当前线程
            Node t = tail;					// 获取尾结点
            node.setPrevRelaxed(t);         // 避免不必要的围栏(fence)
            if (t == null)
                tryInitializeHead();		// 尾结点不存在,则队列不存在,应当初始化队列,内部采用CAS算法
            else if (!casTail(t, node))		// 否则,调用casTail方法入列失败
                node.setPrevRelaxed(null);  // 则应当退出(back out)
            else
                t.next = node;				// 尾插
        } else if (first && spins != 0) {
            --spins;                        // 重新等待时减少不公平性
            Thread.onSpinWait();			// 自旋等待
        } else if (node.status == 0) {
            node.status = WAITING;          // 启用信号(signal)和重检查
        } else {
            long nanos;
            spins = postSpins = (byte)((postSpins << 1) | 1);
            if (!timed)						// 非限时模式,直接调用 park 挂起该线程
                LockSupport.park(this);
            else if ((nanos = time - System.nanoTime()) > 0L)	// 限时模式,检查超时时间参数是否合法
                LockSupport.parkNanos(this, nanos);	// 参数合法,则暂时挂起该线程,直至超时,除非许可可用
            else							// 限时模式,但参数非法(小于等于0)
                break;						// 循环终止
            node.clearStatus();				// 清除结点状态
            if ((interrupted |= Thread.interrupted()) && interruptible)
                // 线程获取不到锁时,根据参数决定是否中断。若中断,则应当中断,终止循环
                break;
        }
    }
    
    // 线程获取不到锁(超时参数非法或等待时中断),取消获取
    // 在当前情况下,传递参数时,要求该方法在非限时模式下,以不中断线程的形式反复尝试获取锁,因此,这条语句不执行
    return cancelAcquire(node, interrupted, interruptible);
}

调用 acquire 方法取锁的过程:(独占模式)

首次执行该方法(node = null, head = null, tail = null, first = false, pred = null),在第一个分支 if (!first && (pred = (node == null) ? null : node.prev) != null && ...) 处,由于第二个操作数在为 pred 赋值后仍为 null,第三个操作数被短路不执行,仍有 first = false

接着来到第二个分支 if (first || pred == null),进入分支,会调用 tryAcquire 方法尝试获取锁。若成功获取(但尚未入列),因此直接方法直接返回 1,线程成功取锁。但若抛出异常,则会取消获取,并向外抛出异常,方法异常结束。但若未抛出异常,且未能够成功取锁,则跳出该分支,继续执行。

接着来到第三个分支 if (node == null),结点不存在,则进入分支创建独占模式结点。第一轮循环结束。

第二轮循环node = ExclusiveNode, head = null, tail = null, first = false, pred = null):进入第一个分支 if (!first && (pred = (node == null) ? null : node.prev) != null && ...)。这时,初始化 pred,得到 pred = node.prev = null,表达式被短路,来到下一分支。

下一分支 if (first || pred == null) 仍满足,继续调用 tryAcquire 方法取锁,流程同上。若未能成功取锁,且尚未抛出异常,则来到下一分支。

第三分支 if (node == null) 不满足,继续判断 else if (pred == null),条件满足,进入分支,尝试入列。让结点 node(ExclusiveNode) 封装当前线程 current,获取尾结点(t = tail)。由于 tail == null,则会调用 tryInitializeHead() 方法初始化队列。该方法内部采用 Unsafe 类中的 CAS 算法(compareAndSetReference(o, offset, expected, x)),创建一个头结点 h(ExclusiveNode),并让尾结点 tail 同时指向 h。至此,队列初始化完毕,进入第三轮循环。

第三轮循环node = ExclusiveNode, head = h, tail = h, first = false, pred = null):进入第一个分支 if (!first && (pred = (node == null) ? null : node.prev) != null && ..),表达式仍然被短路,进入第二分支。

第二分支 if (first || pred == null) 仍满足,继续调用 tryAcquire 方法取锁。若在未抛出异常的情况下未能取到锁,进入第三分支。

第三分支 if (node == null) 不满足,继续判断,在 else if (pred == null) 满足,由于队列已经初始化,因此会执行内部分支中的 casTail(t, node)t = tail),利用 CAS 算法,修改尾结点,让尾结点指向包装当前线程的结点。修改成功后,采用尾插法的方式插入队列。若尾结点修改失败,则一直循环执行该步骤直至成功。第三轮循环结束。

第四轮循环node = ExclusiveNode, head = h, tail = node, first = false, pred = head):进入第一个分支 if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))),该布尔表达式的值为 truefirst = true,意为新插入节点(当前结点)为队列第一个结点,则会进入分支,执行操作。

接着来到第二分支 if (first || pred == null),由于 first = true,表达式满足进入分支。再次尝试调用 tryAcquire 获取锁。若未能成功取锁,且未抛出任何异常,则来到下一分支。

第三个分支 if (node == null) 不满足,最终来到 else if (node.status == 0) 进入分支。让线程进入等待状态。第四轮循环结束。

第五轮循环及其后:总是不断地在第二个选择分支中反复调用 tryAcquire 尝试获取锁,不成功则一直处于等待状态。成功后,鉴于该结点已经进入队列,且该结点为队列中第一个线程结点,则清除该结点信息(prev, next, waiter 设为 null),让头结点 head 指向 node,以便于唤醒队列中下一个线程(通过 first 变量判断)。

总结:最终线程是否能够拿到锁,取决于 tryAcquire 的执行情况。多次循环反复通过调用 tryAcquire 未能成功取锁时,最终该线程会被封装到 ExclusiveNode 中并插入等待队列中。取锁、插队(尾插法)、初始化队列的操作都通过 CAS 算法完成,保证操作原子性。另外,队列中的首个结点(由变量 first 追踪)或未入列的线程总是会不断地尝试 tryAcquire 取锁。位于队列的线程成功取锁后,清除封装该线程的结点信息并将头结点指向自己,以便于实时追踪队列中的首个结点并尝试让下一个线程获取锁。

5-8.7 释放锁的过程

释放锁,则需要在 finally 语句块中调用 unlock 方法。方法会在内部调用 Syncrelease(int) 方法:

//ReentrantLock 中重写自 Lock 的释放锁方法
public void unlock() {
    sync.release(1);
}

//release(int) 方法来自于 AQS 的final方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {//tryRelease由子类实现该方法,AQS中的该方法默认抛出异常UnsupportedOperationException
        //由Sync重定义tryRelease方法,且为final方法
        signalNext(head);
        return true;
    }
    return false;
}

方法中调用了 signalNext(head) 方法,该方法的具体实现为:

// 调用者传递参数:signalnext(head)
private static void signalNext(Node h) {
    Node s;
    // 条件:存在头结点(列表初始化),且 队列中存在线程结点,并且该结点线程不在等待状态中
    if (h != null && (s = h.next) != null && s.status != 0) {
        s.getAndUnsetStatus(WAITING);	// 注:WAITING 为 1,采用位运算,反设置状态值
        LockSupport.unpark(s.waiter);	// 调用 Unsafe 的 unpark 方法,让指定线程不会被阻塞
    }
}

// Node 所定义的 getAndUnsetStatus 方法
// 调用者传入参数:getAndUnsetStatus(1)
final int getAndUnsetStatus(int v) {     // 用于发送信号(for signalling)
    return U.getAndBitwiseAndInt(this, STATUS, ~v);
    // 即:getAndBitwiseAndInt(this, STATUS, 11111111_11111111_11111111_11111110)
    // ~v 即为 -2
}

// Unsafe 类中的 getAndBitwiseAndInt 方法
// 原子地将所给对象中的某个字段或数组元素的当前值替换为当前值与掩码的按位与结果
public final int getAndBitwiseAndInt(Object o, long offset, int mask) {
    int current;
    // CAS 算法
    do {
        current = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset,
                                   current, current & mask));
    return current;	//返回修改前的值
}

5-8.8 Unsafe 与线程调度

在前面的文章中,我们提到 Unsafe 用于执行低级别的不安全操作。该类提供了大量的 CAS 算法用于原子包中。但除了 CAS 算法,该类还定义了一些与线程调度有关的方法。

方法 描述
native void park(boolean isAbsolute, long time) 阻塞线程
native void unpark(Object thread) 取消阻塞线程

AQS 中调用 LockSupportparkunpark 方法时,实际上就是调用了 Unsafe 的对应方法。

posted @ 2023-09-30 12:13  Zebt  阅读(97)  评论(0)    收藏  举报