欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Lock(四) — 可重入锁(ReentrantLock)源码解析

程序员文章站 2022-05-05 23:13:56
...

一、概述

重入锁: 当该锁被线程A持有时,线程A仍可以重新获取该锁。
公平锁: 在获取锁时,如果线程A先对锁进行获取,则当锁释放后,线程A优先获得锁。
非公平锁: 在获取锁时,如果线程A先对锁进行获取,则当锁释放后,线程A不一定先得到锁。

通常情况下,非公平锁效率比公平锁效率高。

  1. 在获取公平锁时,如果同步队列内已经有等待的线程时,那么会优先给同步队列内的线程,而此时请求获取锁的线程就进入同步队列等待,存在线程切换开销。
  2. 在获取非公平锁时,会优先给当前请求获取锁的线程机会,如果获取锁成功就直接返回(降低了线程切换的开销),否则将当前获取锁的线程加入同步队列进行等待。

二、ReentrantLock 类图

ReentrantLock 类图
Lock(四) — 可重入锁(ReentrantLock)源码解析
上图是 ReentrantLock 类图,从图中可知:

  1. ReentrantLock 实现了 Lock 接口,因此具备了锁的相关操作。
  2. ReentrantLock 内部包含一个继承了 AbstractQueuedSynchronizerSync 同步器,因此所有用户态的 Lock 操作都委托给 Sync 来执行。
  3. Sync 有两个子类:FairSyncNonfairSync,他们实现了锁获取策略的公平性问题。
  4. 在 ReentrantLock 中使用到了 AQS ,可以参考:
    1. Lock(二) — 队列同步器(AQS)浅析
    2. Lock(三) — 自定义AQS (实践)

三、源码解析

1. ReentrantLock 的使用

获取锁和释放锁的操作使用 before...after 模式,且获取锁的操作不放在 try 中。

ReentrantLock lock = new ReentrantLock();
lock.lock(); // before
try {
	// 执行逻辑操作
} finally {
	lock.unlock(); //after
}

2 ReentrantLock 的构造

默认使用的是非公平锁的策略

// 默认的 ReentrantLock 使用的是非公平锁的策略
public ReentrantLock() {
    sync = new NonfairSync();
}

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

3. ReentrantLock 锁的获取

在获取锁时,会调用到 AQS 的 acquire() 方法。该方法的具体解析请参考文章: Lock(二) — 队列同步器(AQS)浅析

3.1 非公平锁的获取

// ReentrantLock.class
public void lock() {
    sync.lock(); // sync分公平和非公平锁两种
}

// ReentrantLock.NonfairSync.class
final void lock() {
	// 非公平锁:让当前获取锁的线程优先尝试获取锁操作(这里第1次)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	// 执行到这里说明上面获取锁没有成功
        acquire(1); //acquire(1)方法是AQS内部的
}

// AbstractQueuedSynchronizer.class
public final void acquire(int arg) {
	/* 
	 * 分2步进行判断:
	 * 1.tryAcquire(arg):尝试获取锁(获取锁成功,则直接返回)
	 * 2.addWaiter(Node.EXCLUSIVE):当tryAcquire(arg)获取锁失败后,则使当前线程进入同步队列等待。
	 * 3.acquireQueued(node, arg):使当前获取锁的线程进入自旋,循环判断是否可以获取到锁,并返回是否中断该线程的标记。
	 * 说明:acquireQueued()方法内部不响应线程的中断操作,所以将是否中断操作抛出,在这里执行。
	 */
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        //如果返回中断,则调用当前线程的interrupt()方法
        selfInterrupt();
}

// ReentrantLock.NonfairSync.class
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires); //非公平锁的获取
}

// ReentrantLock.Sync.class
// 主要分为下面3个步骤
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); //1.获取锁是否已经被其他线程持有(0表示该锁没有被线程持有)。
    if (c == 0) { //2.当c==0时,表示该锁没有线程持有
    	/*
    	 * 2.1 这里存在多个线程同时操作的问题,所以得通过CAS原子操作更新state状态。
    	 * 一旦state更新成功,则其他线程在执行到c==0时或通过CAS再次更新state状态时,就会返回false。
    	 */
        if (compareAndSetState(0, acquires)) {  // (非公平锁:这里第2次尝试让当前线程获取锁)
        	// 2.2 将当前持有锁的线程保存到AQS的变量中。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    /*
     * 3.执行到这里,表明锁被线程持有(该线程可能是持有锁的线程,也可能是其他线程)
     * 这里判断当前的线程跟持有锁的线程是否是相同,如果相同,就再次获取锁成功(锁的可重入性);
     */
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // 3.1 这里增加state表示重入次数。
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc); //3.2 这里由于当前线程跟持有锁的线程相同,所以不存在竞争,因此不需要通过CAS操作更新state状态。
        return true;
    }
    return false;
}

3.2 公平锁的获取

// ReentrantLock.class
public void lock() {
    sync.lock(); // sync分公平和非公平锁两种
}

// ReentrantLock.FairSync.class
final void lock() {
	acquire(1); //acquire(1)方法是AQS内部的
}

// AbstractQueuedSynchronizer.class
public final void acquire(int arg) {
	// 该方法在非公平锁获取时已经阐述,此处不解释。
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        selfInterrupt();
}

// ReentrantLock.FairSync.class
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); //1.获取锁是否已经被其他线程持有(0表示该锁没有被线程持有)。
    if (c == 0) {//2.当c==0时,表示该锁没有线程持有。
    	/*
    	 * 2.1 hasQueuedPredecessors():表示判断同步队列中是否有其他线程在等待获取锁(true=同步队列有其他线程在等待)。
    	 * 说明:这个方法可以保证获取锁的公平性,先到先得,后来的线程在同步队列等待(队列特点:FIFO)。
    	 */
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) { //2.2 通过CAS操作更新state状态。
            setExclusiveOwnerThread(current); //2.3 将当前持有锁的线程保存到AQS的变量中。
            return true;
        }
    }
    /*
     * 3.执行到这里,表明锁被线程持有(该线程可能是持有锁的线程,也可能是其他线程)
     * 这里判断当前的线程跟持有锁的线程是否是相同,如果相同,就再次获取锁成功(锁的可重入性);
     */
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc); //更新状态 state表示线程重入次数。
        return true;
    }
    return false;
}

4. ReentrantLock 锁的释放

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

// AbstractQueuedSynchronizer.class
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.class
protected final boolean tryRelease(int releases) {
    int c = getState() - releases; //1.这里每释放一次,state就要减小releases数量(可重入性)
    // 2.这里判断释放锁的线程是否是当前线程
    if (Thread.currentThread() != getExclusiveOwnerThread()) 
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { //3.当state==0,说明该锁没有被任何线程持有
        free = true;
        setExclusiveOwnerThread(null); //4.将独占锁关联的线程信息移除
    }
    setState(c); //因为第2步已经判断是否是同一个线程,所以这里的操作是线程安全的。
    return free;
}

四、小结

1. 如何实现公平锁?

在获取锁的时候,通过 hasQueuedPredecessors() 方法判断同步队列内 是否存在 其他线程也在等待获取锁资源。

  1. 若存在: 这说明有比当前线程早的,为了公平性,则需要让他们先获得锁,而将自己添加进入同步队列进行等待。
  2. 若不存在: 说明当前线程可以直接获取锁资源,当前线程是最先获取锁的。

2. 如何实现非公平锁?

非公平锁提供了两次优先于同步队列获取锁的机会。

第一次: 在调用 lock() 方法时,先尝试让当前线程直接通过 compareAndSetState(0, 1) (CAS)操作来更新 state 状态,更新成功则说明当前线程获取到锁。
第二次: 在第一次没有获取到锁时,在 tryAcquire() 方法中,再次尝试让当前线程直接通过 compareAndSetState(0, 1) (CAS)操作来更新 state 状态,更新成功则说明当前线程获取到锁。

两次都没有成,则将当前线程放入同步队列等待。

注意:

  1. 不管是共配锁,还是非公平锁,同步队列获取锁都是有先后顺序的。
  2. 非公平锁只是给 当前线程 提供了2次优先竞争锁的机会。
相关标签: Java并发编程