2 s锁是什么_JUC并发编程 - StampedLock 乐观锁分析及锁转换

553a8d6f8c4b15ed53984ff73bcc8905.png

概述

在前文当中,针对StampedLock的读锁和写锁的实现进行了分析。这篇文章我们来看一下乐观读锁的实现,以及锁是如何进行转换的。

Fred:JUC并发编程 - StampedLock读写锁分析​zhuanlan.zhihu.com
f283a56f908bfab0fdb4a166f4a81264.png

乐观读锁的使用

所谓乐观锁,是相对应悲观锁而言的,在传统的锁概念下,以代码执行为例,一旦持有锁,在释放之前,其他申请锁的操作都将会阻塞。而乐观锁是以版本号为基础,同步场景下持有乐观锁的线程,会持有一个版本号,在这个版本号不变的前提下,一起操作与非同步场景一致,当遇到写操作时,需要对版本号进行验证,通过后才能继续写,否则要更新同步场景下的变量后,才能继续操作。

以上是我个人对乐观锁比较直白的描述。回顾乐观锁使用的例子。

double distanceFormOrigin() {
	long stamp = sl.tryOptimisticRead(); 
	double currentX = x, currentY = y;
	if (!sl.validate(stamp)) { 
		stamp = sl.readLock();
		try {
			currentX = x;
			currentY = y;
		} finally {
			sl.unlockRead(stamp);
		}
	}
	return Math.sqrt(currentX * currentX + currentY * currentY);
}

在例子中,采用如下方式,获取乐观锁->版本号。

long stamp = sl.tryOptimisticRead();

在需要进行同步操作前,要验证版本号。

sl.validate(stamp) // 乐观锁版本是否已经发生变化。

还可以对锁进行升级,比如拿到读锁。

stamp = sl.readLock();

使用完读锁后,要释放。

sl.unlockRead(stamp);

获取和释放读锁,前文已经分析过,这里不再重复。针对乐观锁,可以采用while循环方式,达到获取最新值目的:

double distanceFormOrigin2() {
	long stamp = sl.tryOptimisticRead(); 
	double currentX = x, currentY = y;
	while (!sl.validate(stamp)) {
		currentX = x;
		currentY = y;
	}
	return Math.sqrt(currentX * currentX + currentY * currentY);
}

接下来分析一下tryOptimisticRead()和validate()的源码。

public long tryOptimisticRead() {
	long s;
	return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; 
}

只是判断了当前STATE的值,

(((s = state) & WBIT) == 0L)

1、为false说明:有写锁状态,返回0

2、为true说明:没有写锁,则返回(s & SBITS),也就是 读锁溢出值,当然如果无锁,则返回的也是0

其实无论返回的是什么,乐观锁应该是肯定能获取到的,因为都返回了一个值=版本号。

public boolean validate(long stamp) {
	U.loadFence(); // 加载屏障,要等load完成
	return (stamp & SBITS) == (state & SBITS);
}

实际工作,就是验证stamp是否等于state的值。

似乎是很简单的逻辑,但申请乐观锁时,为何只返回(s & SBITS),也就是 读锁溢出值?

查看整个StampedLock源码,涉及到乐观锁的方法有如下几个:

tryOptimisticRead // 获取乐观读锁
validate // 验证乐观锁版本号

tryConvertToWriteLock // 转换为读锁
tryConvertToReadLock // 转换为写锁
tryConvertToOptimisticRead // 转换为乐观读锁

对于网上给出的使用例子,当乐观锁版本已经发生变化后,采用了申请读锁的方式。

假设版本号没有发生变化,此时希望做写操作,实际上对其他线程并没有约束力,因为只有一个版本号,而修改的内容并不是StampedLock可控制的。

这种场景,就需要将乐观锁转换为写锁,这时就用到了tryConvertToWriteLock()。

转换为悲观写锁

在使用的例子中,也有一个悲观读锁,升级为写锁的场景。

//悲观读锁以及读锁升级写锁的使用
void moveIfAtOrigin(double newX,double newY) {
	
	long stamp = stampedLock.readLock(); //悲观读锁
	try {
		while (x == 0.0 && y == 0.0) {
			long ws = stampedLock.tryConvertToWriteLock(stamp); //读锁转换为写锁
			if (ws != 0L) { //转换成功
				
				stamp = ws; //票据更新
				x = newX;
				y = newY;
				break;
			} else {
				stampedLock.unlockRead(stamp); //转换失败释放读锁
				stamp = stampedLock.writeLock(); //强制获取写锁
			}
		}
	} finally {
		stampedLock.unlock(stamp); //释放所有锁
	}
}

两种场景都是升级到写锁,也就是调用tryConvertToWriteLock(),分析一下源码:

在分析前,要明确tryConvertToWriteLock()方法,并不是针对乐观锁的,只是它支持乐观锁的转换;从注释看,支持三种场景:

如果锁状态与给定的戳记匹配,则执行以下操作之一。
1、如果戳记表示持有写锁,则返回它。
2、或者,如果有读锁,如果写锁可用,则释放读锁并返回写戳。
3、或者,如果是乐观读,则仅在立即可用时才返回写戳记。【我们的场景】
此方法在所有其他情况下都返回零。

看看源码是否是这么回事。

// return: 0L 代表转换失败。>0L代表申请到,并得到新的版本号
public long tryConvertToWriteLock(long stamp) {
	long a = stamp & ABITS, m, s, next;
	while (((s = state) & SBITS) == (stamp & SBITS)) { // 版本号没变
		if ((m = s & ABITS) == 0L) { // 无锁判断
			if (a != 0L)// stamp不为0,中断
				break;
			if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
				return next;
		}
		else if (m == WBIT) { // 写锁
			if (a != m)
				break;
			return stamp;
		}
		else if (m == RUNIT && a != 0L) {
			if (U.compareAndSwapLong(this, STATE, s,
									 next = s - RUNIT + WBIT))
				return next;
		}
		else
			break;
	}
	return 0L;
}

逐行分析:

ABITS  -> 0000 0000 0000 0000 0000 0000 1111 1111
  • if ((m = s & ABITS) == 0L),无锁或读锁溢出
  • if (a != 0L),前文申请乐观锁时,存在两种情况返回0L,1、有写锁,2、无锁。当a=stamp,不为0,说明是有读锁的场景,而前面if已经判定为无锁,因此a的值有问题,break。
  • if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) 尝试对STATE累加写锁,成功则获得新的版本号,转换成功。
  • else if (m == WBIT) ,存在写锁场景
  • if (a != m),stamp与state不相等,版本号不一致,异常情况,要结束
  • return stamp,既然当前存在写锁,那么直接返回stamp就行了。
  • else if (m == RUNIT && a != 0L), RUNIT是读锁单位,只有一个读锁
  • if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT)) // 先减再加,相当于释放读锁再加写锁
  • else,其他情况不支持转换,break。

转换为悲观读锁

乐观锁OptimisticRead转换为读锁tryConvertToReadLock,先看一下方法注释含义:

如果锁状态与给定的戳记匹配,则执行以下操作之一。
1、如果戳记表示持有写锁,则释放它并获得读锁。
2、或者,如果是读锁,则返回它。
3、或者,如果一个乐观读操作获得了一个读锁,并且只有在立即可用的情况下才返回一个读戳。
此方法在所有其他情况下都返回零。

逻辑与写锁转换的差不多,对于乐观锁的使用场景下,所谓立即可用,可以理解为CAS操作成功吧。

public long tryConvertToReadLock(long stamp) {
	long a = stamp & ABITS, m, s, next; WNode h;
	while (((s = state) & SBITS) == (stamp & SBITS)) { // 版本号没变
		if ((m = s & ABITS) == 0L) {
			if (a != 0L)
				break;
			else if (m < RFULL) {
				if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
					return next;
			}
			else if ((next = tryIncReaderOverflow(s)) != 0L)
				return next;
		}
		else if (m == WBIT) {
			if (a != m)
				break;
			state = next = s + (WBIT + RUNIT);
			if ((h = whead) != null && h.status != 0)
				release(h);
			return next;
		}
		else if (a != 0L && a < WBIT)
			return stamp;
		else
			break;
	}
	return 0L;
}

逐行分析:

ABITS  ->0000 0000 0000 0000 0000 0000 1111 1111
  • if ((m = s & ABITS) == 0L),无锁或读锁溢出
  • if (a != 0L),前文申请乐观锁时,存在两种情况返回0L,1、有写锁,2、无锁。当a=stamp,不为0,说明是有读锁的场景,而前面if已经判定为无锁,因此a的值有问题,break。
  • (m < RFULL) 并且 if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) 尝试对STATE累加读锁,成功则获得新的版本号,转换成功。
  • else if ((next = tryIncReaderOverflow(s)) != 0L) ,读锁又溢出,溢出处理后得到新的版本号
  • else if (m == WBIT) ,存在写锁场景【比较复杂,在写锁场景转换为读锁,摘出来分析】
  • if (a != m),stamp与state不相等,版本号不一致,异常情况,要结束
  • state = next = s + (WBIT + RUNIT), 新的版本号,是当前版本号 加锁读锁单位和写锁单位,由于此时是转换为读锁,因此
  • if ((h = whead) != null && h.status != 0) 判断头节点的状态
  • release(h),当前操作线程肯定是持有锁的线程,上面涉及到写锁释放,这里要操作whead进行释放动作。
  • else if (a != 0L && a < WBIT), a != 0L 说明有读锁,a < WBIT 说明读锁没有溢出,这种情况不用convert,直接返回stamp即可。
  • else,其他情况不支持转换,break。

转换为乐观读锁

顺便在看一下tryConvertToOptimisticRead()方法,转换为乐观读锁,先看看注释分析。

如果锁状态与给定的戳记匹配,
如果戳记表示持有锁,则释放它并返回一个观察戳记。
或者,如果是乐观读取,则在验证后返回。
该方法在所有其他情况下都返回0,因此可以作为“tryUnlock”的一种形式使用。

描述的有些模糊,在翻译下大概是,

1、如果当前有读锁,将读锁释放并返回当前state。

2、如果本身就是乐观锁,则验证一下是否正确,再返回。

看看源码是否是这么回事。

public long tryConvertToOptimisticRead(long stamp) {
	long a = stamp & ABITS, m, s, next; WNode h;
	U.loadFence(); // 屏障,应该是保障有序性
	for (;;) {
		if (((s = state) & SBITS) != (stamp & SBITS)) // 验证版本号是否变化
			break;		
		if ((m = s & ABITS) == 0L) {
			if (a != 0L)
				break;
			return s; // 直接返回,对应情况2
		}
		else if (m == WBIT) { // 写锁
			if (a != m)
				break;
			state = next = (s += WBIT) == 0L ? ORIGIN : s; // 释放写锁
			if ((h = whead) != null && h.status != 0) 
				release(h);// 释放头节点
			return next;
		}
		else if (a == 0L || a >= WBIT) // 无锁或者 读锁溢出 ,结束
			break;
		else if (m < RFULL) { // 读锁
			if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT)) { // 释放读锁
				if (m == RUNIT && (h = whead) != null && h.status != 0) // 释放头节点,对应情况1
					release(h);
				return next & SBITS; 
			}
		}
		else if ((next = tryDecReaderOverflow(s)) != 0L) // 释放还是溢出的情况
			return next & SBITS;
	}
	return 0L;
}

逐行分析。

ABITS  -> 0000 0000 0000 0000 0000 0000 1111 1111
  • if ((m = s & ABITS) == 0L),无锁、读锁溢出,总之保证低7位是0
  • if (a != 0L),前文申请乐观锁时,存在两种情况返回0L,1、有写锁,2、无锁。当a=stamp,不为0,说明是有读锁的场景,而前面if已经判定为无锁,因此a的值有问题,break。
  • return s; // 直接返回,对应情况2
  • else if (m == WBIT) , 写锁
  • if (a != m) 版本号发生变化,中断。
  • state = next = (s += WBIT) == 0L ? ORIGIN : s ,释放写锁
  • if ((h = whead) != null && h.status != 0) release(h);,判断头节点状态,释放头节点
  • else if (a == 0L || a >= WBIT), 无锁或者 读锁溢出 ,结束
  • else if (m < RFULL),读锁
  • if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT)) ,尝试释放读锁
  • if (m == RUNIT && (h = whead) != null && h.status != 0) release(h); ,释放头节点,对应情况1
  • else if ((next = tryDecReaderOverflow(s)) != 0L),释放还是溢出的情况
SBITS	-> 1111 1111 1111 1111 1111 1111 1000 0000 = -128

next & SBITS,的值就是高位的值,也就是读锁溢出的值,与tryOptimisticRead中(s & SBITS)含义相同,这个值可能就是注释中描述的“观察戳记”。

有待讨论:

1、其实这其中对于“观察戳记”含义,还是要有些模糊,为何乐观锁的版本号,使用高位溢出的读锁数量即可。

2、写锁转换为读锁的场景,涉及到state值的转换,深层含义是什么:state = next = s + (WBIT + RUNIT);

3、读锁转换为乐观锁的场景,涉及到state值的转换,深层含义是什么:state = next = (s += WBIT) == 0L ? ORIGIN : s;

写锁转换为读锁的场景

state = next = s + (WBIT + RUNIT);

运算:

WBIT			-> 0000 0000 0000 0000 0000 0000 1000 0000 = 128
RUNIT			-> 0000 0000 0000 0000 0000 0000 0000 0001 = 1
WBIT + RUNIT		-> 0000 0000 0000 0000 0000 0000 1000 0001 = 129

WBIT + RUNIT,只是代表了当前既有写锁又有一个读锁

s=state此时代表的是写锁场景,state=WBIT

s			-> 0000 0000 0000 0000 0000 0000 1000 0000 = 128
+ (WBIT + RUNIT)        -> 0000 0000 0000 0000 0000 0000 1000 0001 = 129
next			-> 0000 0000 0000 0000 0000 0001 0000 0001 = 257

这个值改变了state的既有意义,变成了读锁溢出状态。而我们假定s中没有读锁溢出的,也就是高位都是0,但如果不为0,也不影响结果。

这个位运算的妙处就在于它通过写锁位的变化,完成了读写转换。

读锁转换为乐观锁的场景

state = next = (s += WBIT) == 0L ? ORIGIN : s;

此时state的含义也是写锁m=s=WBIT

WBIT				-> 0000 0000 0000 0000 0000 0000 1000 0000 = 128
s 				-> 0000 0000 0000 0000 0000 0000 1000 0000 = 128
s +=WBIT			-> 0000 0000 0000 0000 0000 0001 0000 0000 = 256

通过进位,完成了写锁转换为读锁。

(s += WBIT) == 0L,可能为true吗?我暂时没想到这个场景,但Dog Lee肯定是有想法的。

release(h); 就是释放h节点的锁,改变state的值,不再赘述。

对于读锁、写锁,还有一套方法tryXXXLock,tryUnlockXXX,尝试获取读锁、写锁,尝试释放读锁、写锁。思路是CAS修改state的值,并且失败场景不入队,直接返回。

三个视图类

在StampedLock中,还存在ReadLockView、WriteLockView以及ReadWriteLockView三个内部类,它们在实现时,采用的都是StampedLock的方法,但单独列出来的含义不太清楚,猜测是作为快照使用,由于StampedLock默认是读写锁模式,内部的asReadLock和asWriteLock则是获取对应xxxView实例,应该是剥离读写关联的逻辑,分离出一个单独性质的锁对象,而ReadWriteLockView返回的则是ReadLockView和WriteLockView的实例,由asReadWriteLock得到。

在StampedLock中定义了三个视图类,都实现了Lock接口或ReadWriteLock接口,也就是实现了锁的功能,看看它们的内部结构。

final class ReadLockView implements Lock {
	public void lock() { readLock(); }
	public void lockInterruptibly() throws InterruptedException {
		readLockInterruptibly();
	}
	public boolean tryLock() { return tryReadLock() != 0L; }
	public boolean tryLock(long time, TimeUnit unit)
		throws InterruptedException {
		return tryReadLock(time, unit) != 0L;
	}
	public void unlock() { unstampedUnlockRead(); }
	public Condition newCondition() {
		throw new UnsupportedOperationException();
	}
}

final class WriteLockView implements Lock {
	public void lock() { writeLock(); }
	public void lockInterruptibly() throws InterruptedException {
		writeLockInterruptibly();
	}
	public boolean tryLock() { return tryWriteLock() != 0L; }
	public boolean tryLock(long time, TimeUnit unit)
		throws InterruptedException {
		return tryWriteLock(time, unit) != 0L;
	}
	public void unlock() { unstampedUnlockWrite(); }
	public Condition newCondition() {
		throw new UnsupportedOperationException();
	}
}

final class ReadWriteLockView implements ReadWriteLock {
	public Lock readLock() { return asReadLock(); }
	public Lock writeLock() { return asWriteLock(); }
}

分析其调用链,可以发现封装的都是StampedLock中实现的方法,并且ReadLockView和WriteLockView类似于一组,而ReadWriteLockView则是调用的asXXXLock方法。

尾声

本文对于StampedLock的主要方法进行了分析,在使用其读锁、写锁和乐观锁时,可以作为一些参考,毕竟理解其深层含义才能更好使用。当然,本文的分析属于一家之言,某些地方可能有些片面,大家共同学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值