源码分析【ReentrantLock】原理

2023-11-06

ReentrackLock介绍

ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比

synchronized更加灵活(具有更多的方法)。

区别:

  • 1.ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。
  • 2.ReentrantLock可以实现公平和非公平锁。
  • 3.ReentantLock获取锁时,限时等待,配合重试机制更好的解决死锁
  • 4.ReentrantLock可响应中断

先来看看继承关系

img

基于AOS的加锁解锁流程,底层队列中阻塞唤醒线程采用的LockSupport类的park()和unpark()方法

img

非公平锁VS公平

非公平锁

之前我认为公平与非公平的区别在于当一个线程持有锁执行完成之后,队列中阻塞的线程都可以抢占锁,其实不是,源码中是这样的,只有在新进来线程的时候会与队首线程进行竞争,这时可能会不公平,新进来的可能会插队,如下图所示:

image-20211009200332427

image-20211009200400466

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

源码中是这样体现的

ReentrantLock的tryAcquire方法中

final Thread current = Thread.currentThread();
int c = getState();
// 如果还没有获得锁
if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
	if (compareAndSetState(0, acquires)) {
		setExclusiveOwnerThread(current);
		return true;
	}
}
公平锁

而公平锁的源码tryAcquire方法是这样的 if判断中多了一个hasQueuedPredecessors方法去先检查 AQS

队列中是否有前驱节点, 没有才去竞争

// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
		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;
}

image-20211009205240316

可打断VS不可打断

可打断:队列中线程等待锁的过程中,其他线程可以用Interupt方法终止该线程的等待。

可打断模式应用的场景在于希望阻塞的线程不要一直阻塞下去,避免死锁的一种办法。

接着分析源码

不可打断【默认】
 ReentrantLock lock = new ReentrantLock();
 lock.lock();

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

如果当前线程没有抢到锁会调用acquireQueued方法进行park

注意下面关键两点:

  • 调用Thread.interrupted()会清楚打断标记,即将打断标记置为false
  • park的线程被打断后会自动unpark,并将打断标记置为true,只要打断标记为true就永远不会park住
// park
final boolean acquireQueued(final Node node, int arg) {
    // 拿锁失败?默认是。
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) 
            // 如果获得不了锁,T1 依旧会进入 park。
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);  
                p.next = null; 
                failed = false;
                // T1 只有拿到锁时,才能跳出这个死循环。
                // 返回 打断标记,true。
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&            
                parkAndCheckInterrupt())
                // 被打断的 T1 执行到这里。
                interrupted = true;
            	// 死循环,回到上面去重新执行。
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 线程停在了这里。
    // 获取当前线程是否被打断了,并且清空打断标记。
    return Thread.interrupted();
}

可以看到源码中通过一个死循环,来保证没有获得到锁的线程park中,而在线程被打断后没获得到锁

后就会 return Thread.interrupted(),清空打断标记,死循环下次进入parkAndCheckInterrupt()中park住线

程直到获得锁后return Thread.interrupted();

最后,进入if判断,再将打断标记置为true。

可打断模式
ReentrantLock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();
} catch (InterruptedException e) {
    e.printStackTrace();
}

源码分析:

static final class NonfairSync extends Sync {
	public final void acquireInterruptibly(int arg) throws InterruptedException {
		if (Thread.interrupted())
			throw new InterruptedException();
		// 如果没有获得到锁, 进入 ㈠
		if (!tryAcquire(arg))
			doAcquireInterruptibly(arg);
	}
// ㈠ 可打断的获取锁流程
	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()) {
					// 在 park 过程中如果被 interrupt 会进入此
					// 这时候抛出异常, 而不会再次进入 for (;;)
						throw new InterruptedException();
				}
			}
		} finally {
			if (failed)
				cancelAcquire(node);
		}
	}
}

可打断模式下主要是一旦被打断就抛出打断异常,这样就不会继续死循环了

锁超时

为避免park住的线程一直park,还有一种解决办法就是设置锁超时

ReentrantLock lock = new ReentrantLock();
lock.tryLock(1, TimeUnit.MINUTES);

锁超时下是可打断的

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
    	// 打断抛出异常
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

在doAcquireNanos方法中记录了park的时间,一旦超过传入的时间,就之间返回false,线程unpark

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 (;;) {
            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;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

条件变量

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject实现了Condition接口

public class ConditionObject implements Condition, java.io.Serializable {
	private transient Node firstWaiter;
	private transient Node lastWaiter;
}

image-20211009212305822

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的

通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的

Condition必须用newCondition()方法。

这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建

立不同的Condition

Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以

创建多个Condition,在不同的情况下使用不同的Condition。

例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出

数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线

程"需要等待。

如何在synchronized和ReentrantLock之间进行选择

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。

与ReentrantLock相比,内置锁的一个优点是:在线程转储中能给出在哪些调

用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。JVM并不知道哪

些线程持有ReentrantLock,因此在调试使用ReentrantLock的线程的问题

时,将起不到帮助作用。ReentrantLock的非块结构特性仍然意味着,获取

锁的操作不能与特定的栈帧关联起来,而内置锁却可以。未来更可能会提升

synchronized而不是ReentrantLock的性能。因为synchronized是JVM的内置

属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增

加锁的粒度来消除内置锁的同步,而如果通过基于类库的锁来实现这些功

能,则可能性不大。

欢迎关注微信公众号与我交流

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

源码分析【ReentrantLock】原理 的相关文章

随机推荐