ReentrantLock源码导读

497 阅读8分钟

ReentrantLock是JDK5开始提供的Java实现的显式锁,它是一个可重入锁,依赖于AQS实现。它使用AQS的state变量作为锁的重入次数,每lock()一次state就自增1,每unlock()一次state就自减1,当state减至0时,就代表锁释放了。

当锁释放后,AQS会调用unparkSuccessor()去唤醒队列中的后继节点去竞争锁,锁竞争失败的线程AQS会创建一个和线程绑定的Node节点,入队并Park挂起线程。

线程同步的细节在AQS类中就已经实现,ReentrantLock只需要编写很少的代码就可以快速实现一个Java同步锁。

注:ReentrantLock是基于AQS来实现的,在此之前必须先了解AQS的源码,笔者的另一篇文章:《AQS源码导读

属性

ReentrantLock的属性很简单,内部类Sync是基于AQS实现的一个同步器,有两种实现:公平的FairSync、非公平的NonfairSync。

// 基于AQS实现的同步器,有公平和非公平两种实现。
private final Sync sync;

构造函数

ReentrantLock默认使用非公平锁,这样效率会高一些,但是存在线程饥饿的情况,也可以通过构造函数指定使用公平锁实现。

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

/*
fair指定使用公平锁/非公平锁
 */
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

核心方法

lock

加锁的操作会调用sync.lock(),公平锁和非公平锁的逻辑是不一样的:

/*
竞争锁,根据锁的类型,调用tryAcquire()或nonfairTryAcquire()。
 */
public void lock() {
	sync.lock();
}

非公平锁:上来直接就抢锁,不管队列中有没有线程在等待。

/*
非公平锁的lock,上来直接就抢锁,不管队列中有没有线程在等待。
 */
final void lock() {
	// CAS的方式将修改state,如果修改成功,表示没有其他线程持有锁,将当前线程设为独占锁的持有者
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		// CAS失败,代表其他线程已经持有锁了,此时去竞争锁
		acquire(1);
}

公平锁:调用AQS的acquire()方法:

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

acquire()是AQS里的一个模板方法,它首先会调用tryAcquire()去尝试获得锁,如果失败则创建节点,入队并Park线程:

/*
竞争锁的流程:
1.tryAcquire():再次尝试去获取锁。
2.addWaiter():如果还获取不到,在队列的尾巴添加一个Node。
3.acquireQueued():去排队。
 */
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		// 是否需要进行一次自我中断,来补上线程等待期间发生的中断。
		selfInterrupt();
}

公平锁的tryAcquire()非常有礼貌,即使当前是无锁状态,也要判断队列中是否有线程已经在等待了,只要前面有线程在排队,自己就放弃抢锁,入队挂起:

// 公平锁-尝试获取锁
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		/*
		即使当前是无锁状态,也要判断队列中是否有线程已经在等待了。
		如果有其他线程在等待,要让其他线程先获取锁,自己入队挂起。
		如果队列中无线程,则尝试CAS竞争。
		 */
		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;
}

而非公平锁就不管队列中有没有线程等待了,上来就CAS抢锁:

/*
非公平锁尝试获取锁。
如果竞争成功,则直接返回,失败则交给AQS处理。
AQS会创建一个和当前线程绑定的Node节点,入队并Park。
 */
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// state==0,代表其他线程已经释放锁了,再次CAS的方式修改state,成功则代表抢到锁,返回true。
		if (compareAndSetState(0, acquires)) {
			// 设置当前线程为锁的持有线程。
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 如果当前线程就是锁的持有线程,则重入,state++。
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) // 锁不可能无限重入,重入的次数超过了int最大值后,就会溢出。
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	// 其他线程没释放锁,当前线程又不是持有锁的线程,则抢锁失败。
	return false;
}

抢锁失败的话,AQS会创建一个和当前线程绑定的Node节点,将节点插入到队列的尾巴处,并调用LockSupport.park()将当前线程挂起,直到持有锁的线程释放锁后,AQS才会唤醒等待队列中的线程。 这所有的操作都是在AQS中完成的,因此ReentrantLock的代码量并不多,要想了解AQS的实现细节,可以查看笔者的另一篇文章:《AQS源码导读》。

tryLock

线程调用lock()方法后,如果抢不到锁会一直死等,如果代码写的有问题,容易出现死锁的情况。 ReentrantLock提供了一种更为灵活的方式:tryLock()。 它可以在不阻塞的情况下,去尝试获取锁,获取到了就返回true,获取失败就返回false。 也可以在给定一个超时时间内,尝试获取锁,获取不到锁就超时退出,不再竞争锁,避免了死锁的情况。

tryLock() 非阻塞的方式,尝试获取锁,直接调用nonfairTryAcquire(),判断AQS的state是否等于0,如果是就说明无锁,通过CAS的方式将state从0修改为1,修改成功就代表抢到锁了,返回true,否则返回false。

/*
以非公平的方式,尝试竞争锁。
仅当无锁时才会成功,不会自旋重试。
 */
public boolean tryLock() {
	return sync.nonfairTryAcquire(1);
}

tryLock(long timeout, TimeUnit unit) 给定一个超时时间,在这个时间内去尝试获取锁,和lock()的区别是,线程不会被无期限的挂起,而是调用LockSupport.parkNanos()挂起指定的时间,超时后自动唤醒,如果还是获取不到锁就返回false并退出竞争。

/*
在给定超时时间内尝试获取锁。
和lock()流程类似:
1.首先会调用tryAcquire()尝试获取一下,如果获取不到,则创建Node节点入队。
2.调用LockSupport.parkNanos()挂起当前线程指定时间。
3.超时后还获取不到锁,则返回false,同时AQS会移除对应的Node节点。
 */
public boolean tryLock(long timeout, TimeUnit unit)
		throws InterruptedException {
	return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

tryAcquireNanos()又是AQS里的模板方法,它首先会调用tryAcquire()去尝试获取锁,如果获取不到,就调用doAcquireNanos()将线程入队并挂起。

/**
 * 以独占的模式,在一个超时时间内获取
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
		throws InterruptedException {
	if (nanosTimeout <= 0L)
		return false;
	// 计算到期时间
	final long deadline = System.nanoTime() + nanosTimeout;
	// 创建Node节点,入队
	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;
			// 判断抢锁失败后是否要挂起,前提是将前驱节点的waitStatus设为SIGNAL
			if (shouldParkAfterFailedAcquire(p, node) &&
					nanosTimeout > spinForTimeoutThreshold)
				// 挂起指定时间
				LockSupport.parkNanos(this, nanosTimeout);
			if (Thread.interrupted())
				// 这个过程是响应中断的,如果发生了中断则抛异常
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			// 超时了,如果抢锁还是失败,则取消竞争节点
			cancelAcquire(node);
	}
}

lockInterruptibly

lock()是不响应中断的,阻塞期间即使对线程调用了interrupt()也是不予理会的,AQS只会在竞争到锁后补上一次自我中断。 而lockInterruptibly()是响应中断的,当其他线程对当前线程调用interrupt()后,会将其从Park中唤醒,并抛出InterruptedException异常。

/*
lock()是不响应中断的,即使对线程调用了interrupt()也是不予理会的,
AQS只会在竞争到锁后补上一次自我中断。
lockInterruptibly()是响应中断的,当其他线程对当前线程调用interrupt()后,
会将其从Park中唤醒,并抛出InterruptedException异常。
 */
public void lockInterruptibly() throws InterruptedException {
	sync.acquireInterruptibly(1);
}

acquireInterruptibly()又是AQS的模板方法,它首先会调用tryAcquire()尝试获取锁,获取失败则调用doAcquireInterruptibly()已可中断的方式去获取锁。 逻辑和acquireQueued()差不多,就是对待线程中断的方式不一样。 acquireQueued()面对线程中断,只是打一个标记,待获得锁后补上一次自我中断。 doAcquireInterruptibly()面对线程中断,直接抛出InterruptedException异常,退出竞争。

/**
 * 独占模式,可中断的获取资源
 */
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);
    }
}

unlock

解锁,只有锁的持有线程才可调用,否则会抛出IllegalMonitorStateException异常。 ReentrantLock是可重入锁,unlock()就是state自减的过程,只有当state减至0,才代表锁真正释放了。

/*
解锁,如果当前线程是持有锁的线程,则state自减1,减少至0时则释放锁,并唤醒队列中的节点。
如果当前线程不是持有锁的线程,那就是非法解锁了,会抛IllegalMonitorStateException异常。
 */
public void unlock() {
	sync.release(1);
}

release()是AQS的模板方法,它会调用子类的tryRelease(),返回true代表锁真正被释放了,AQS会调用unparkSuccessor()去唤醒等待队列中的线程。

// 释放锁
public final boolean release(int arg) {
	/*
	调用子类的tryRelease(),返回true代表成功释放锁。
	对于ReentrantLock来说,state减少至0代表需要释放锁。
	 */
	if (tryRelease(arg)) {
		/*
		head就代表持有锁的节点。
		如果head的waitStatus!=0,说明有后继节点在等待被其唤醒。
		还记得线程入队时,如果要挂起,必须将其前驱节点的waitStatus改为-1吗???
		如果节点入队不改前驱节点的waitStatus,它将无法被唤醒。
		 */
		Node h = head;
		if (h != null && h.waitStatus != 0)
			// 释放锁后要去唤醒后继节点
			unparkSuccessor(h);
		return true;
	}
	return false;
}

ReentrantLock判断锁释放的条件很简单,就是state==0

/*
尝试释放锁,返回true代表锁成功释放,AQS会唤醒后继节点。
只有持有锁的线程才能释放锁,因此不存在并发问题。
 */
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;
}

其他方法

/*
获取当前线程锁的重入次数,非持有锁的线程返回0
 */
public int getHoldCount() {
	return sync.getHoldCount();
}

/*
当前线程是否是锁的持有线程
 */
public boolean isHeldByCurrentThread() {
	return sync.isHeldExclusively();
}

/*
是否是加锁状态,判断依据就是:state!=0
为什么不是state>0?
存在state数值溢出的情况。
 */
public boolean isLocked() {
	return sync.isLocked();
}

/*
是否是公平锁
 */
public final boolean isFair() {
	return sync instanceof FairSync;
}

/*
返回锁的持有线程,无锁时返回null
 */
protected Thread getOwner() {
	return sync.getOwner();
}

/*
是否有线程在等待锁?
判断依据:AQS队列的头节点!=尾节点
 */
public final boolean hasQueuedThreads() {
	return sync.hasQueuedThreads();
}

/*
指定线程是否在等待获取锁?
遍历AQS队列,判断Thread对象是否相等。
 */
public final boolean hasQueuedThread(Thread thread) {
	return sync.isQueued(thread);
}

/*
返回等待线程数的估计值。
遍历AQS队列中线程不为null的节点,累加计算得到。
不准确,因为节点数据可能在遍历期间发生改变。
 */
public final int getQueueLength() {
	return sync.getQueueLength();
}

/*
返回可能在等待锁的线程集合。
遍历AQS队列中线程不为null的节点,将线程添加到ArrayList中返回。
不准确,因为节点数据可能在遍历期间发生改变。
 */
protected Collection<Thread> getQueuedThreads() {
	return sync.getQueuedThreads();
}

/*
条件队列中是否有等待线程?
遍历ConditionObject队列中的节点,判断节点状态是否为Node.CONDITION。
 */
public boolean hasWaiters(Condition condition) {
	if (condition == null)
		throw new NullPointerException();
	if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
		throw new IllegalArgumentException("not owner");
	return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
}

/*
返回条件队列中可能等待的线程数
遍历ConditionObject队列中的节点,判断节点状态是否为Node.CONDITION,累加计算。
 */
public int getWaitQueueLength(Condition condition) {
	if (condition == null)
		throw new NullPointerException();
	if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
		throw new IllegalArgumentException("not owner");
	return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
}

/*
返回条件队列中可能等待的线集合。
遍历ConditionObject队列中的节点,判断节点状态是否为Node.CONDITION,将线程添加到ArrayList中返回。
 */
protected Collection<Thread> getWaitingThreads(Condition condition) {
	if (condition == null)
		throw new NullPointerException();
	if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
		throw new IllegalArgumentException("not owner");
	return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
}

总结

AQS设计的真的非常优秀,模板方法模式,大部分的逻辑都在底层做掉了,子类只需要编写简单的代码就可以实现一个好用的同步工具类。

ReentrantLock就是在AQS的基础上扩展的一个用Java语言编写的同步锁,它使用AQS的state变量作为锁的重入次数,线程能不能获取锁,就看CAS能否将state从0改为1,竞争锁失败的线程AQS底层会将其入队并挂起。待持有锁的线程释放锁后,AQS会自动唤醒等待队列中的线程,这一切ReentrantLock都不需要操心。