工作之后,我对 ReentrantLock 有了新的理解

写在前面的话

本文会介绍 ReentrantLock 的核心源码以及常见问题,过程中会涉及到一点点 AQS 的部分,但是只会做简单的介绍。建议在读本文时不要发散开去学 AQS,相关内容后续会专门梳理一遍。

基础概念

发音

[riː’entrəntlɒk]

类图

在这里插入图片描述

开始阅读源码之前,一定要先从整体上看一下类图,本文的重点就是标颜色的模块。

  1. ReentrantLock 的核心是内部类 Sync。
  2. Sync 继承于 AQS,AQS 又继承于 AbstractOwnableSynchronizer。
  3. Sync 有两个子类,分别实现了公平锁和非公平锁。

ReentrantLock 为什么称作可重入锁

首先我们同步一下认知:
可重入锁是一个概念,ReentrantLock 是满足这个条件的具体实现,并不是说只有 ReentrantLock 才是可重入锁。我是个男人,但男人不仅仅只有我一个。
可重入锁,指的是【同一个线程】可以对【同一个资源】重复地加锁,而不需要经历先释放再加锁的操作。先有个概念就够了,通过下文的源码理解我们再去了解可重入具体的操作指的是什么。

那什么锁是不可重入的

既然有可重入锁,那必然有对应的不可重入锁,此外还有各种乱七八糟的概念如偏向锁、内置锁、乐观锁等等。这些都不是互斥的概念,都是不同场景下满足某一个特征的叫法,千万不要在此展开,本文我们就只关注可重入。

公平锁与非公平锁

上面刚说了先不展开其他的锁,怎么突然又来了一对新概念?
因为 ReentrantLock 的核心内部类是 Sync,而 Sync 对应两个不同的实现类,也就是公平锁与非公平锁,不讲不行。
从字面意思上看,公平锁就是公平的,非公平锁就是不公平的,真的是一句酣畅淋漓的废话哈哈。
公平,体现在当前线程去尝试拿锁的时候,是否需要关注其他线程的优先级更高。如果是公平锁,则需要礼让其他优先级更高的线程,非公平锁则不在意其他线程,一有机会就拿锁,相当于插队。此外,关于怎么定义这个优先级,下文也会简单提一下。
这么看起来,非公平锁是不正义的,而正义,只在射程之内!

AbstractQueuedSynchronizer

Sync 类继承的父类,也就是卷死的 AQS 的全称,在 ReentrantLock 的实现中就大量依赖了 AQS 提供的能力所以简单提一。
AQS 值得单独写一篇文章,本文不会深入 AQS 的细节,先简单理解为它提供了一个线程排队的队列,利用这个队列先进先出的特性来实现所谓的公平。

AbstractOwnableSynchronizer

AQS 又继承于 AbstractOwnableSynchronizer,简单看一下源码其实很短,它的作用就是包含了持有了当前的锁的线程信息。

在这里插入图片描述


源码理解(JDK1.8)

核心变量

ReentrantLock 核心变量就一个 Sync 类型的对象,也就是说对应的能力全部基于 sync 对象展开,继续往下看。

public class ReentrantLock implements Lock, java.io.Serializable {  
    private static final long serialVersionUID = 7373984872572414699L;
    // ReentrantLock 持有的是一个 Sync 类型的变量  
    /** Synchronizer providing all implementation mechanics */  
    private final Sync sync;
    // 省略
}
}

构造方法

ReentrantLock 有两个构造方法,他们的区别在于 Sync 对象的实现类,一个是 NonfairSync 对象,另一个则根据入参决定,为 true 时对应 FailSync 对象,false 时对应 NonfairSync 对象。


public ReentrantLock() {  
    sync = new NonfairSync();  
}

public ReentrantLock(boolean fair) {  
    sync = fair ? new FairSync() : new NonfairSync();  
}


Sync 类

Sync 是 ReentrantLock 中定义的一个内部类,它的子类分为公平实现和非公平实现,使用 AQS 状态来获取锁的保持次数。这里先不用关注具体的实现逻辑,瞄一眼就继续往下看吧,方法的大概功能先都写在注释上了。

/**  
 * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */
abstract static class Sync extends AbstractQueuedSynchronizer {  
    private static final long serialVersionUID = -5179523762034025860L;  
      
    abstract void lock();

	// 以非公平的方式获取锁
    final boolean nonfairTryAcquire(int acquires) {  
       // 先忽略
    }  
	// 释放锁
    protected final boolean tryRelease(int releases) {  
    // 先忽略
    }  
    // 判断当前是否为锁的持有者
    protected final boolean isHeldExclusively() {  
     return getExclusiveOwnerThread() == Thread.currentThread();  
    }   
  
    // 获取当前锁的持有线程
    final Thread getOwner() {  
        return getState() == 0 ? null : getExclusiveOwnerThread();  
    }  
	// 获取锁的持有次数
    final int getHoldCount() {  
        return isHeldExclusively() ? getState() : 0;  
    }  
	// 判断当前是否处于上锁状态
    final boolean isLocked() {  
        return getState() != 0;  
    }  
}


Sync 的公平锁和非公平锁实现 & lock() 方法

公平锁子类
static final class FairSync extends Sync {  
    private static final long serialVersionUID = -3000897897090466540L;  
  
    final void lock() {  
        acquire(1);  
    }    
}
非公平锁子类
static final class NonfairSync extends Sync {  
    private static final long serialVersionUID = 7316153563782823691L;  
  
    /**  
     * Performs lock.  Try immediate barge, backing up to normal     * acquire on failure.     */    
    final void lock() {
	    // 非公平锁的 lock 方法中,直接就开始通过 CAS 的方式获取锁(就是setState)         // 如果上锁成功,则直接设置当前线程为持有者了。
	    // 如果上锁失败,才会调用 acquire() 方法。
        if (compareAndSetState(0, 1))  
            setExclusiveOwnerThread(Thread.currentThread());  
        else  
            acquire(1);  
    }  
}

仔细对比一下公平锁 FairSync 和非公平锁 NonfairSync 这两者中 lock() 的实现,非公平锁的实现中一开始就尝试 CAS 获取锁,完全没有对当前的锁状态和其他线程进行判断,这就是为什么叫非公平的原因,我来了就要拿锁,我不管别人怎么样。
而公平锁没有这么做,而是直接调用 acquire() 方法。


acquire() 方法
public final void acquire(int arg) {  
    if (!tryAcquire(arg) &&  
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
        selfInterrupt();  
}

tryAcquire 方法在公平锁 FairSync 和非公平锁 NonfairSync 的子类中都进行了重写,
对比一下差异其实就在 if (c=0) 的下一行中,即是否判断队列中有等待的线程。

	公平锁中的实现
/**  
     * Fair version of tryAcquire.  Don't grant access unless     * recursive call or no waiters or is first.     */    
    protected final boolean tryAcquire(int acquires) {  
        final Thread current = Thread.currentThread();  
        int c = getState();
        // 如果当前处于无锁状态  
        if (c == 0) {
	        // 关键:如果队列中有其他线程排在当前线程之前,则判断 
	        // hasQueuedPredecessors()=true,注意前面还有一个 !,此外因为
	        // 短路机制所以后面的 compareAndSetState 也不会执行。
            if (!hasQueuedPredecessors() &&  
                compareAndSetState(0, acquires)) {  
                setExclusiveOwnerThread(current);  
                return true;  
            }  
        }
        // 此时处于有锁状态,判断当前的持有者如果已经是当前线程的话,则累加 state 值 
        else if (current == getExclusiveOwnerThread()) {  
            int nextc = c + acquires;  
            if (nextc < 0)  
                throw new Error("Maximum lock count exceeded");  
            setState(nextc);  
            return true;  
        }  
        return false;  
    } 
  protected final boolean tryAcquire(int acquires) {  
        return nonfairTryAcquire(acquires);  
    }  

非公平锁中的实现
/**  
 * Performs non-fair tryLock.  tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */
 final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) {  
	     // 关键:对比于公平锁,这里没有判断等待队列中是否有其他线程排在前面,
	     // 直接通过 CAS 尝试拿锁,如果 CAS 操作成功则当前线程直接就截胡拿到锁了。
        if (compareAndSetState(0, acquires)) {  
            setExclusiveOwnerThread(current);  
            return true;  
        }  
    }  
    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;  
}

tryLock() 方法

上面已经讲了公平锁和非公平锁分别对 lock() 方法的实现,我们再来看下 tryLock 。tryLock 也有两个不同的实现,先看没有参数的这个

public boolean tryLock() {
	// 直接调用 nonfairTryAcquire 尝试拿锁,上文已经分析过这个方法,是非公平的。
    return sync.nonfairTryAcquire(1);  
}

在来看带超时时间的 tryLock(),首先要注意,这个方法会抛出 InterruptedException 异常。

public boolean tryLock(long timeout, TimeUnit unit)  
        throws InterruptedException {
        // 直接调用 tryAcquireNanos 方法。
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));  
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)  
        throws InterruptedException {
        // 先判断线程是否中断,是的话直接抛异常了  
    if (Thread.interrupted())  
        throw new InterruptedException();
       // tryAcquire 尝试拿锁,上面讲过了。重点看下 doAcquireNanos 方法   
    return tryAcquire(arg) ||  
        doAcquireNanos(arg, nanosTimeout);  
}

doAcquireNanos 的实现在 AbstractQueuedSynchronizer 类中,复习一下,这是个线程等待队列,简单概括一下就是

  • 在超时时间内循环得去判断当前等待队列中的其他线程,如果轮到当前线程了则尝试加锁。
  • 在这个过程中去判断超时时间以及线程是否被中断,如果超时则返回,中断则抛异常。
  • 其他详情写在注释了。
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 (;;) {  
	        // 获取当前节点 node 的前驱节点 p
            final Node p = node.predecessor();  
            // 如果已经是头节点了,则尝试获取锁,获取成功则世界返回 true
            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;
			// shouldParkAfterFailedAcquire 先简单理解为将当前节点的
			// 前驱节点设置一个标记,如果轮到它了则触发通知。
			// 此外,如果剩余超时时间大于 spinForTimeoutThreshold(常量,1000纳秒) ,则直接让当前线程进入阻塞休眠。如果小于这个时间了,则继续开始尝试拿锁过程。
            if (shouldParkAfterFailedAcquire(p, node) &&  
                nanosTimeout > spinForTimeoutThreshold)  
                LockSupport.parkNanos(this, nanosTimeout);  
            if (Thread.interrupted())  
                throw new InterruptedException();  
        }  
    } finally {
	    // 最后别忘了,如果尝试拿锁失败了,则将当前的 node 节点删除。  
        if (failed)  
            cancelAcquire(node);  
    }  
}

unlock() 方法

unlocl 就是释放当前线程锁的过程,概括一下

  • 调用 tryRelease 方法对 state 值进行递减,如果减到 0 则说明当前线程对锁的持有已经完全释放。
  • 当前线程释放锁之后,判断等待队列中是否还有其他在排队的线程,有的话则唤醒下一个节点。
public void unlock() {  
    sync.release(1);  
}

public final boolean release(int arg) {
    // 直接调用 tryRelease 方法
    if (tryRelease(arg)) {
	    // 当 tryRelease 返回 true,即释放锁成功,此时再判断等待队列中是否还有
	    // 等待中的线程节点,有的话则调用 unparkSuccessor 唤醒下一个排队中的线程。
        Node h = head;  
        if (h != null && h.waitStatus != 0)  
            unparkSuccessor(h);  
        return true;  
    }  
    return false;  
}

protected final boolean tryRelease(int releases) {
	// 对 state 值进行递减
    int c = getState() - releases;
	// 判断当前线程是否为资源的持有者,不是的话当然不能释放锁,直接抛异常。
    if (Thread.currentThread() != getExclusiveOwnerThread())  
        throw new IllegalMonitorStateException();  
    boolean free = false;
    // 当 c 的值递减到 0,则说明锁已经释放,返回 free=true 并设置资源的当前持有者
    // 为 null  
    if (c == 0) {  
        free = true;  
        setExclusiveOwnerThread(null);  
    }  
    setState(c);  
    return free;  
}

state 到底是什么?

至此,关于加锁与释放锁的代码流程已经讲完了,当时我有一个困惑,所谓的加锁操作到底是什么意思?又是在哪里体现了重入呢?加锁和释放的代码中一直在更新一个 state 值,它又是什么?

我们再来瞄一眼加锁和释放锁的过程,从始至终都是对 state 这个值的读写。

final void lock() {
	// CAS 更新 state=1
    if (compareAndSetState(0, 1))  
        setExclusiveOwnerThread(Thread.currentThread());  
    else  
        acquire(1);  
}

protected final boolean tryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();
    // 获取 state 的值  
    int c = getState();  
    if (c == 0) {  
    // 忽略
    return false;  
}

protected final boolean tryRelease(int releases) {  
    int c = getState() - releases;  
    if (Thread.currentThread() != getExclusiveOwnerThread())  
        throw new IllegalMonitorStateException();  
    boolean free = false;  
    if (c == 0) {  
        free = true;  
        setExclusiveOwnerThread(null);  
    }  
    setState(c);  
    return free;  
}

state 字段是 AQS 类 AbstractQueuedSynchronizer 中的一个字段,注意使用了 volatile 关键字修饰。
加锁和释放锁的操作本质上就是对 state 字段的赋值,加锁时执行 +1,释放锁时执行 -1。
当 state=0 时标志当前资源处于无锁状态,state=1 则表示有一个线程正在持有,而 state > 1 时则表示同一个线程多次加锁成功,这也就是重入的本质。

/**  
 * The synchronization state. */
 private volatile int state;
getHoldCount() 方法

getHoldCount() 方法实质上就是获取了 state 的值,注意限制了当前线程只能获取自身的持有锁的次数,若不是当前线程则直接返回 。

public int getHoldCount() {  
    return sync.getHoldCount();  
}

final int getHoldCount() {  
    return isHeldExclusively() ? getState() : 0;  
}
isLocked() 方法

isLocked() 方法 本质上就是判断 state 值是否为 0

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

final boolean isLocked() {  
    return getState() != 0;  
}

总结

ReentrantLock 中是如何使用 AQS 的?

ReentrantLock 继承 AQS ,利用的是 AQS 提供的线程队列,当多个线程在竞争的时候会进入此队列中排队,通过这个队列辅助锁的分配。
AQS 中的关键字段也使用 volatile 字段修饰,以此来保证多线程竞争时的可见性。


加锁、释放锁和重入对应的实际操作是什么?

  • 加锁,对应 AQS 中 state 变量的递增。
  • 释放锁,对应 AQS 中 state 变量的递减。
  • 重入,当前资源的持有者如果正好是当前线程的话,则累加 state 值。如果不是同一个线程的话,则没有重入的概念。

State 的数值对应的含义

  • 0,表示资源当前处于无锁状态。
  • 1,表示资源正在被某一个线程所持有。
  • 大于1,表示同一个线程在持有锁之后,又再次拿到了锁,也就是重入了,此时 state 值就会递增。假设此时 state=n,则表示当前线程重入了 n-1 次锁。
  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值