多线程系列-6 ReentrantLock源码

背景:

在并发场景下,为了保证线程安全,需要进行加锁操作。
在使用ReentrantLock时需要手动加锁解锁,这一点区分于Synchronized的自动释放。
所以在使用ReentrantLock时一定要遵循java编码规范,在try块钱调用lock()方法获取锁,在finnally块中调用unlock()方法释放锁,这样保证锁一定能被正确释放。Synchronized代码块是通过monitorenter 和 monitorexit 指令来实现的,ReentrantLock的实现原理呢?
本文重点在于结合源码介绍ReentrantLock的实现原理。这个过程中会涉及到AQS,针对涉及到的点进行AQS相关的介绍。

ReentrantLock源码介绍

下面进入ReentrantLock原码分析,ReentrantLock实现了Lock接口。

1.lock

public interface Lock {
    void lock();
    
    void lockInterruptibly() throws InterruptedException;
    
    boolean tryLock();
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

包括了4个加锁接口,一个释放锁的接口,以及一个生成Condition对象的接口。源码分析就从这些加锁、解锁接口进入并深入,至于newCondition()在JAVASE-3 多线程体系2-Condition与ConditionObject中详细介绍。

2.ReentrantLock

先整体看一下ReentrantLock这个类的内部组成:

public class ReentrantLock implements Lock, Serializable {
    private final ReentrantLock.Sync sync;

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

    public ReentrantLock(boolean isFair) {
        this.sync = (ReentrantLock.Sync)(isFair? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }

    static final class FairSync extends ReentrantLock.Sync { // 省略FairSync逻辑}

    static final class NonfairSync extends ReentrantLock.Sync { // 省略NonfairSync逻辑}

    abstract static class Sync extends AbstractQueuedSynchronizer{ // 省略Sync逻辑}
   
   // 此处省略ReentrantLock的其他方法,lock系列,unlock,...
}

内部定义了一个Sync类,该类继承自AQS;定义了两个Sync的子类:FairSync和NonfairSync,从名字可以看出这两个子类的区别在于获取锁的策略是否公平。从默认构造函数可以看出,ReentrantLock默认支持不公平的锁策略。

在简单看一下ReentrantLock关于Lock的实现:

private final ReentrantLock.Sync sync;

public void lock() {
	this.sync.lock();
}

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

public boolean tryLock() {
	return this.sync.nonfairTryAcquire(1);
}

public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException {
	return this.sync.tryAcquireNanos(1, var3.toNanos(var1));
}

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

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

可以看出,Lock中提供的方法最终都调用了sync的方法来实现,ReentrantLock就是提供了一层封装而已。ReentrantLock就是基于AQS来实现的,因此本文不可避免地需要接触AQS。
由于这部分涉及的概念较多,且代码量比较大,因此只对涉及内容进行简单讲解。本文涉及的ReentrantLock是一个独占锁,因此对AQS的介绍会有所聚焦;对AQS内容感兴趣,可以参考:JAVASE-3 多线程体系10-AQS源码
ReentrantLock不仅是独占性质的锁,从名字可以看出来,还具有可重入性。这两点在AQS中体现在两个实例变量上:Thread类型的exclusiveOwnerThread 和 int类型的state。
ReentrantLock的sync继承自AQS,每个ReentrantLock实例都对应一个sync实例变量,因此每个ReentrantLock对象多对应一个exclusiveOwnerThread和state变量, 即每个ReentrantLock都有一把锁。
exclusiveOwnerThread 和 state 的使用方式如下:

当没有线程获取锁时,exclusiveOwnerThread为null,state等于1;
当锁被线程获取时,exclusiveOwnerThread指向获取锁的线程,state变成1;
当该线程再次获取锁时,state加1;释放锁的时候,也是没释放一次,state减1。
这里介绍的内容都会在下文的代码解析里体现。首先看一下NonfairSync和FairSync的继承体系:
在这里插入图片描述
从上图可以看出,Sync中定义了nonfairTryAcquire(int)tryRelease(int)
NonfairSync和FairSync中分别重写了lock()和tryAcquire(int)。
因此释放锁,没有公平与否的概念,走的都是Sync中的tryRelease(int)逻辑。
接下来我们先看一下非公平锁是怎么实现的,再对比一下公平锁。

非公平锁 lock()

final void lock() {
    acquire(1);
}

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

lock() 直接调用AQS的acquire(1);acquire中有一个条件!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这里的tryAcquire就是上面提到的NonfairSync和FairSync自己重载的方法。
我们看一下NonfairSync的tryAcquire方法:

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

非公平锁的tryAcquire直接调用了Sync类中定义的nonfairTryAcquire方法。
因此非公平锁的lock()主要实现逻辑就在nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (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;
}

先获取state的值,如果是state的值等于,0表示没有被加锁:则使用CAS方式将state加1,
然后当前线程的值赋值给exclusiveOwnerThread。
如果不等于0,表示锁已经被线程占用,这时需要判断是不是自己占用了锁:current == getExclusiveOwnerThread();如果是该线程占用了锁,则是重入,对state加1,否则,尝试加锁失败-返回false。
=> tryAcquire(int args) 就是一次尝试加锁,如果加锁成功-返回true,加锁失败返回false;
继续回到!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
tryAcquire(arg)加锁成功了,就不会执行后面的条件判断了,直接退出;
加锁失败后,进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg),首先执行addWaiter(Node.EXCLUSIVE),将当前线程加入到等待队列中:

private Node addWaiter(Node mode) {
	Node node = new Node(Thread.currentThread(), mode);
	Node pred = tail;
	if (pred != null) {
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	enq(node);
	return node;
}

第一步构造一个Node对象new Node(Thread.currentThread(), mode),mode是Node.EXCLUSIVE,为null,是一个标志位(在AQS中独占锁用null表示,new Node()表示共享锁,存在nextWaiter变量里);
接下来:

if (pred != null) {
		Node pred = tail;
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}

cas需要和自旋配合才可以起作用,这里只是进行了一次判断,如果可以使用cas将新创建的节点设置为尾节点,则操作成功,并退出。如果一次不成功呢?
没关系,还有enq()方法:

private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		if (t == null) {
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}

enq()就是一个自旋+cas的组合,将新创建的节点加入到同步队列的尾部;
这里是不是有点疑惑,那为什么addWaiter上多此一举地加了一次cas判断?
其实是作了一层优化:一次判断如果可以退出,没必要再进自旋块。
=> addWaiter方法时使用当前线程构造一个Node对象,加入同步队列中,并返回该Node对象。
这个Node对象会传参给acquireQueued方法:

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);
	}
}

主体内容在一个自旋块中,线程如果获取不到锁,就会一直阻塞在这个自旋逻辑中,直到成功获取锁。
在介绍这个逻辑之前,先说明一下AQS对于独占锁的唤醒机制:
当前节点用currentNode表示,前驱节点用preNode表示,因此有以下关系:currentNode.prev == preNode;

(1) currentNode由preNode唤醒;
(2) preNode必须时Signal的,才能唤醒currentNode;
(3) 只有当preNode位于同步队列的头结点时,才会触发唤醒currentNode操作;

接下来,我们继续看自旋的主体逻辑线:

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;
	}
}

该自旋逻辑只有一个退出执行点:return interrupted;,且位于p == head && tryAcquire(arg)条件逻辑中。说明只有node的前驱节点是头结点,然后调用tryAcquire获取锁成功时,才会退出该自旋逻辑(获取锁成功后,会把node设置为头结点)。
在看一下第二个条件语句:shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt();注意:这个条件语句也是存在于自旋块中的。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		return true;
	if (ws > 0) {
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

shouldParkAfterFailedAcquire方法比较清晰,使用cas方式将node的前驱节点设置为SIGNAL状态;
同步队列的Node有4种状态:CANCELLED=1, SIGNAL=-1; CONDITION=-2; PROPAGATE=-3;
pred.waitStatus > 0表示该节点所在的线程已经被cancel了;设置前驱节点的过程也会更新同步对类信息,剔除已经被cancel的节点。
在shouldParkAfterFailedAcquire配合着自旋块,将node的前驱节点状态设置为SIGNAL时;再次自旋进入shouldParkAfterFailedAcquire方法会返回true;此时会调用parkAndCheckInterrupt()方法:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

该方法逻辑简单,直接调用LockSupport.park(this);使得该线程休眠,等待被前驱节点唤醒。
=> 至此,非公平整个lock的解锁逻辑以及清楚了。
在此基础上了解公平锁lock就比较简单了。
从下面的图可以看出,NonfairSync和FairSync的区别仅在于lock()和tryAcquire(int),至于获取锁失败后怎么添加到同步队列是没有区别的。
在这里插入图片描述

// 公平锁
final void lock() {
    acquire(1);
}

// 非公平锁
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

对比两个lock()方法:
公平锁按照获取锁的流程调用acquire(1);来获取锁;
非公平锁,直接先试探一下能否获取锁成功,失败了再调用acquire(1);获取锁。
acquire会调用tryAcquire,在前面介绍过,这里不再赘述。正如前文所述,在tryAcquire中非公平锁,直接进行尝试加锁。
我们再看一下公平锁下的加锁策略:

 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;
    }
}

在尝试加锁之前,先调用hasQueuedPredecessors判断是否已经有其他线程在等待锁了,如果没有才会加锁,如果有了返回false表示加锁失败。其实在tryAcquire中已经可以实现公平和非公平加锁的区别了,在非公平锁的lock()里的一次尝试加锁是一层优化。
对比可以发现:
非公平锁,可以避免线程等待时间,省去了释放cpu和获取cpu的时间。
假如线程A准备加锁时,已经有线程B和C在同步队列中等待,这时锁正好被线程D释放;此时A就不需要先去队列排序,再被唤醒,而是直接获取锁;这样从整体的角度,省去了一次线程切换的时间开销,有助于提高程序运行效率。
这样也直接导致了一个问题,从A后面由A1,A2,A3……他们到来的时候,锁正好被释放,那么B和C就会一直等下去,出现线程饥饿线程。

unlock()

unlock在公平锁和非公平锁中没有区分,调用的是同一个方法:

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

进入Sync的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;
}

release方法中涉及tryRelease和unparkSuccessor两个方法:

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;
}

tryRelease方法直接操作state和exclusiveOwnerThread变量,先给state将去需要释放锁的数量,然后判断如果state为0,就将exclusiveOwnerThread置为null。
其中 if (Thread.currentThread() != getExclusiveOwnerThread())是为了保证释放锁的操作需要由只有锁的线程来进行。
当线程占用的锁都释放完成后,即state变为0的时候,release(1)返回true,此时进入

Node h = head;
if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
return true;

这里调用unparkSuccessor(h)唤醒头结点的一个有效的后继节点。因为独占锁只能被一个线程所占用,所以没必要唤醒很多线程,然后让他们去竞争-从而浪费资源。在后续文章中提及的共享锁,和这里有所区分,一次会唤醒多个线程,届时会详细介绍。
我们再看一下unparkSuccessor方法的逻辑:

private void unparkSuccessor(Node node) {
	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);
	}
}

这里头结点的后继节点不为null,则调用LockSupport.unpark(s.thread);激活该线程,否则Node t = tail; t != null && t != node; t = t.prev:从尾结点开始便利,直到遇到一个没有被cancel的节点。
是不是有个疑问:为什么不是释放头结点,而是释放头结点的后继节点?
这里,我们需要结合前面的addWaiter方法一起介绍:
addWaiter方法中,调用enq方法将线程加入到同步队列中,中有一个判断:判断头结点是否为空;

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

t == null表示头结点和尾结点都为null,此时调用compareAndSetHead(new Node())创建了一个新的头结点,后续因获取锁失败而阻塞的线程都会添加在这个头结点之后。这就是为什么当锁完全被释放后,释放节点要从头结点的后继节点开始。所以,释放锁的过程是FIFO,是公平的。

其他方法

除了lock()和unlock()之外,Lock接口中还定义了tryLock(),lockInterruptibly(),tryLock(long var1, TimeUnit var3)方法,是对功能的补充,本质的实现都是依赖于AQS。

tryLock() 和 tryLock(long, TimeUnit)

tryLock()不是一个阻塞方法,如果获取成功返回true, 获去失败返回false:

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

可以看出tryLock()直接调用了上面介绍的nonfairTryAcquire()方法尝试获取锁,是一次性判断(不是自旋),也是一个非公平性质的加锁操作。
至于tryLock(long times, TimeUnit unit)无非是一个具有超时机制的加锁操作,在等待times时间,还没有获取到锁,就返回false;否则加锁成功-返回true。

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

调用的是tryAcquireNanos方法:

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
	if (Thread.interrupted()) throw new InterruptedException();
	return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}

逻辑和acquire方法比较相似,不同点是:在调用tryAcquireNanos过程中,如果线程被中断了会抛出中断异常。
再看一下具有延时功能的doAcquireNanos方法,由于代码与前面介绍的加锁过程极其相似,只挑出主线逻辑进行介绍:

private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
	final long deadline = System.nanoTime() + nanosTimeout;
	final Node node = addWaiter(Node.EXCLUSIVE);
	for (;;) {
		final Node p = node.predecessor();
		if (p == head && tryAcquire(arg)) {
			setHead(node);
			return true;
		}
		nanosTimeout = deadline - System.nanoTime();
		if (nanosTimeout <= 0L)
			return false;
		if (shouldParkAfterFailedAcquire(p, node) &&
			nanosTimeout > spinForTimeoutThreshold)
			LockSupport.parkNanos(this, nanosTimeout);
		if (Thread.interrupted())
			throw new InterruptedException();
	}
} // 省略部分逻辑,突出矛盾的主要方面

除了正常加锁之外,多了一层判断,如果事件超时了,if (nanosTimeout <= 0L),则返回false; 因为超时后,需要自动被唤醒,所以不能调用LockSupport.park方法,此处调用的是LockSupport.parkNanos(this, nanosTimeout);

lockInterruptibly()

看到这个方法就知道这个方法时lock的copy版本,增加一个响应中断的能力。
在介绍lock时未曾提到会抛出InterruptedException异常,事实上,Lock接口提供的加锁操作只有lockInterruptibly()tryLock(long timeout, TimeUnit unit)具有响应中断的能力。

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

调用acquireInterruptibly加锁:

public final void acquireInterruptibly(int arg) throws InterruptedException {
	if (Thread.interrupted()){
		throw new InterruptedException();
	}
	if (!tryAcquire(arg)){
		doAcquireInterruptibly(arg);
	}
}

如果此时线程被中段,就会抛出InterruptedException异常;
加锁成功,会直接返回;如果加锁失败,就会进入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);
	}
}

parkAndCheckInterrupt执行完后,会返回这个过程中线程是否被中断过,这里因为和lock加锁过程一致,不再详细介绍。
但是这里有一处需要看一下:

if (shouldParkAfterFailedAcquire(p, node) &&
	parkAndCheckInterrupt())
	throw new InterruptedException();

相同的逻辑在lock()中是这样的:

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

之后因为返回的interrupted 是true, 而调用Thread.currentThread().interrupt();使线程处于中断状态,而不是抛出异常。

除此之外,还有一个newCondition()方法,用于生成一个Condition对象,这里不再讲解了。放在ConditionObject文章中一起讲解。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值