JUC知识点总结(三)ReentrantLock与ReentrantReadWriteLock源码解析

8. Lock接口 (ReentrantLock 可重入锁)

特性

ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 它是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。

  • 尝试非阻塞地获取锁:tryLock(),调用方法后立刻返回;
  • 能被中断地获取锁:lockInterruptibly():在锁的获取中可以中断当前线程
  • 超时获取锁:tryLock(time,unit),超时返回

Condition 类和 Object 类锁方法区别区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

tryLock 和 lock 和 lockInterruptibly 的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false, tryLock(long timeout, TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
  2. lock 能获得锁就返回 true,不能的话一直等待获得锁
  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

与Synchronized区别

  • ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会
    被 JVM 自动解锁机制不同, ReentrantLock 加锁后需要手动进行解锁。为了避免程序出
    现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操
    作。
  • ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要
    使用 ReentrantLock。

代码示例

public class MyService {
	private Lock lock = new ReentrantLock();
	//Lock lock=new ReentrantLock(true);//公平锁
	//Lock lock=new ReentrantLock(false);//非公平锁
	private Condition condition=lock.newCondition();//创建 Condition
	public void testMethod() {
		try {
			lock.lock();//lock 加锁
			//1: wait 方法等待:
			//System.out.println("开始 wait");
			condition.await();
			//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
			//:2: signal 方法唤醒
			condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
			for (int i = 0; i < 5; i++) {
				System.out.println("ThreadName=" +
				Thread.currentThread().getName()+ (" " + (i + 1)));
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
}

ReentrantLock源码分析

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态。它支持公平锁和非公平锁,两者的实现类似。AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。

参考并发编程——详解 AQS CLH 锁

非公平锁NonfairSync lock()的过程
final void lock() {
	if (compareAndSetState(0, 1))//CAS操作,若state为0则将其设为1
    	setExclusiveOwnerThread(Thread.currentThread());
    else
    	acquire(1);
}
获取锁失败进入acquire(1):
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}
tryAcquire(arg):第一步:尝试去获取锁。
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();//获取state变量值
	if (c == 0) { //没有线程占用锁 :非公平锁的特点
		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); // 更新state值为新的重入次数
		return true;
	}  
	return false; //获取锁失败
}

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

“非公平”即体现在这里,如果占用锁的线程刚释放锁,state为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 第二步:获取锁失败则入队。

addWaiter(Node.EXCLUSIVE)将新节点和当前线程关联并且入队列:

private Node addWaiter(Node mode) {
 	//初始化节点,设置关联线程和模式(独占 or 共享)
	Node node = new Node(Thread.currentThread(), mode);  
	Node pred = tail; // 获取尾节点引用  
	if (pred != null) {// 尾节点不为空,说明队列已经初始化过
		node.prev = pred;    
		if (compareAndSetTail(pred, node)) {//CAS,设置新节点为尾节点
			pred.next = node;
			return node;
		}
	}  
	enq(node); // 尾节点为空,说明队列还未初始化
	return node;
}

private Node enq(final Node node) {  
	for (;;) {//开始自旋
		Node t = tail;
		if (t == null) { // 如果tail为空
			if (compareAndSetHead(new Node()))//新建一个head节点
				tail = head; //tail指向head
		} else {
			node.prev = t;      
			if (compareAndSetTail(t, node)) {// tail不为空
				t.next = node; //将新节点入队
				return t;
			}
		}
	}
}
acquireQueued(final Node node, int arg) 已经入队的线程尝试获取锁,若失败则会被挂起。
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true; //标记是否成功获取锁
	try {
		boolean interrupted = false; //标记线程是否被中断过
		for (;;) {
			final Node p = node.predecessor(); //获取前驱节点
			//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
			if (p == head && tryAcquire(arg)) {
				setHead(node); // 获取成功,将当前节点设置为head节点
				p.next = null; // 原head节点出队,在某个时间点被GC
				failed = false; //获取成功
				return interrupted; //返回是否被中断过
			}
			// 判断获取失败后是否可以挂起,若可以则挂起
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				// 线程若被中断,设置interrupted为true
				interrupted = true;
		}
	} finally {
		if (failed)
		cancelAcquire(node);
	}
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//前驱节点的状态
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 前驱节点状态为signal,返回true
		return true;
		// 前驱节点状态为CANCELLED
		if (ws > 0) {
		// 从队尾向前寻找第一个状态不为CANCELLED的节点
			do {
				node.prev = pred = pred.prev;
			} while (pred.waitStatus > 0);
				pred.next = node;
			} else {
		// 将前驱节点的状态设置为SIGNAL
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);// 挂起当前线程,返回线程中断状态并重置
	return Thread.interrupted();
}

​ 线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

非公平锁NonfairSync unlock()的过程:
public void unlock() {
	sync.release(1);
}

public final boolean release(int arg) {
	if (tryRelease(arg)) {//尝试释放锁
		Node h = head;
		if (h != null && h.waitStatus != 0)//若头结点的状态是SIGNAL
			unparkSuccessor(h);//唤醒头结点下一个节点的关联线程
		return true;
	}
	return false;
}

protected final boolean tryRelease(int releases) {
	int c = getState() - releases; // 计算释放后state值
	// 如果不是当前线程占用锁,那么抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {    
		free = true; // 锁被重入次数为0,表示释放成功    
		setExclusiveOwnerThread(null); // 清空独占线程
	}  
	setState(c); // 更新state值
	return free;
}

tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。

公平锁和非公平锁

公平锁和非公平锁释放时,最后都要写一个volatile变量state

公平锁获取时,首先会去读volatile变量,若为0,按队列顺序获取锁

非公平锁获取时,首先会用CAS更新volatile变量,若为0,当前线程可直接抢占

tryLock():线程获取锁失败后,先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。

ReentrantReadWriteLock 源码分析

ReentrantReadWriteLock包含两个内部类: ReadLock和WriteLock,获取锁和释放锁都是通过AQS来实现的。AQS的状态state是32位的,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数(exclusiveCount)。

线程进入读锁的前提条件:(共享锁)
  • 没有其他线程的拥有写锁,
  • 没有写请求或者有写请求,但调用线程和持有读锁的线程是同一个。
线程进入写锁的前提条件:(排他锁/独占锁)
  • 没有其他线程的读锁
  • 没有其他线程的写锁
读写锁有以下三个重要的特性:
  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 重进入:读锁和写锁都支持线程重进入。
  • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
获取写锁的步骤:

(1)判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取锁,执行(2);否则执行(5)。

(2)若读锁此时被其他线程占用,或其他线程获取写锁,则返回false,当前线程不能获取写锁。

(3)若当前线程获取写锁超过最大次数,抛异常,否则更新同步状态,返回true。

(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。

释放写锁的步骤:

(1)查看当前线程是否为写锁的持有者,如果不是抛出异常。

(2)检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

获取读锁的步骤:

(1)若写锁线程数 != 0 ,且独占锁不是当前线程,则返回失败

(2)否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且CAS设置状态

(3)若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程就是第一个读线程,则为重入增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。

释放读锁的步骤:

(1)判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;

(2)若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。

总结:

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

写锁可以“降级”为读锁;读锁不能“升级”为写锁。

下一篇
JUC知识点总结(四)五种单例模式的写法

发布了54 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/dong_W_/article/details/105009228