Java中的StampedLock实现原理详解
作者:java架构师-太阳
StampedLock(邮戳锁/版本锁/票据锁)
jdk8引入 读读不互斥,读写不互斥,写写互斥
- ReentrantReadWriteLock采用悲观读,第一个读线程拿到锁后,第二个/第三个读线程可以拿到锁,特别是在读线程很多,写线程很少时,写线程可能一直拿不到锁,在非公平锁的情况下,导致写线程饿死
- StampedLock引入乐观读,读时不加读锁,写锁也可进行写,避免写线程被饿死,读数据时读出来发现数据被修改了,再升级为悲观读,再读一次
- 所有获取锁的方法,都返回一个邮戳(Stamp),为0表示获取失败,其余值都表示获取成功
- 所有释放锁的方法,都需要一个邮戳(Stamp),必须和成功获取锁时的一致
stamp是long类型,代表锁的状态,当归0时,表示线程获取锁失败,当释放锁或转换锁时,都要传入最初的stamp值
不可重入,如果一个线程已经获取写锁,再去获取写锁的话容易死锁
三种模式
- read(读悲观模式): 功能和ReentrantReadWriteLock的读锁类似,在读时不允许有写的操作
- write(悲观写模式): 功能和ReentrantReadWriteLock的写锁功能类似
- Optimistic(乐观读模式): 乐观锁,读时可有写操作,读完后检验版本号是否被写改了,若改了就再悲观读一次
代码演示
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 多个线程调用该方法,修改x和y的值
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 多个线程调用该方法,求距离
double distenceFromOrigin() {
// 乐观读
long stamp = sl.tryOptimisticRead();
// 将共享变量拷贝到线程栈, 读:将一份数据拷贝到线程的栈内存中
double currentX = x, currentY = y;
// 读期间有其他线程修改数据, 读取后,对比读之前的版本号和当前的版本号,判断数据是否可用
// 根据stamp判断在读取数据和使用数据期间,有没有其他线程修改数据
if (!sl.validate(stamp)) {
// 读到的是脏数据,丢弃.重新使用悲观读
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
如上代码,有一个Point类,多个线程调用move()方法,修改坐标;还有多个线程调用distanceFromOrigin()方法,求距离 首先,执行move操作时,要加写锁,和ReadWriteLock的用法没有区别,写操作和写操作也是互斥的 在读时,用乐观读sl.tryOptimisticRead(),相当于在读之前给数据的状态做一个快照,把数据拷贝到内存里面,在用之前,再比对一次版本号,如果版本号变了,则说明在读的期间有其他线程修改了数据,读出来的数据废弃,重新悲观读获取数据
要说明的是,这三行关键代码对顺序非常敏感,不能有重排序。因为state变量已经是volatile,所以可以禁止重排序,但stamp并不是volatile的。为此,在validate(stamp)方法里面插入内存屏障
public boolean validate(long stamp) {
VarHandle.acquireFence();
return (stamp & SBITS) == (state & SBITS);
}
public class StampedLockDemo {
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public void write() {
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName()+"\t"+"写线程准备修改");
try {
number = number + 13;
}finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName()+"\t"+"写线程结束修改");
}
//悲观读,读没有完成时候写锁无法获得锁
public void read() {
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName()+"\t"+" come in readlock code block,4 seconds continue...");
for (int i = 0; i < 4; i++) {
// 暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+" 正在读取中......");
}
try {
int result = number;
System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:"+result);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
}finally {
stampedLock.unlockRead(stamp);
}
}
// 乐观读,读的过程中也允许获取写锁介入
public void tryOptimisticRead() {
long stamp = stampedLock.tryOptimisticRead();
int result = number;
// 故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
System.out.println("4秒前stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
for (int i = 0; i < 4; i++) {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"正在读取... "+i+" 秒" +
"后stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)) {
System.out.println("有人修改过------有写操作");
stamp = stampedLock.readLock();
try {
System.out.println("从乐观读 升级为 悲观读");
result = number;
System.out.println("重新悲观读后result:"+result);
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);
}
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
/* 传统版
new Thread(() -> {
resource.read();
},"readThread").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
resource.write();
},"writeThread").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"number:" +number);*/
new Thread(() -> {
resource.tryOptimisticRead();
},"readThread").start();
// 暂停2秒钟线程,读过程可以写介入,演示
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
// 暂停6秒钟线程
// try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
resource.write();
},"writeThread").start();
}
}乐观读实现原理
StampedLock是一个读写锁,因此也会像读写锁那样,把一个state变量分成两半,分别表示读锁和写锁的状态。
同时,还需要一个数据的version,但是,一次CAS没有办法操作两个变量,所以这个state变量本身同时也表示了数据的version。
下面先分析state变量
public class StampedLock implements java.io.Serializable {
private static final int LG_READERS = 7;
private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS; // 第8位表示写锁
private static final long RBITS = WBIT - 1L; // 最低的7位表示读锁
private static final long RFULL = RBITS - 1L; // 读锁的数目
private static final long ABITS = RBITS | WBIT; // 读锁和写锁状态合二为一
private static final long SBITS = ~RBITS;
private static final long ORIGIN = WBIT << 1; // state的初始值
private transient volatile long state;
}
用最低的8位表示读和写的状态,其中第8位表示写锁的状态,最低的7位表示读锁的状态。
因为写锁只有一个bit位,所以写锁是不可重入的
初始值不为0,而是把WBIT 向左移动了一位,也就是上面的ORIGIN 常量,构造方法如下所示
为什么state的初始值不设为0呢?
看乐观锁的实现:
上面两个方法必须结合起来看:当state&WBIT != 0的时候,说明有线程持有写锁,上面的tryOptimisticRead会永远返回0。
这样,再调用validate(stamp),也就是validate(0)也会永远返回false。这正是我们想要的逻辑:当有线程持有写锁的时候,validate永远返回false,无论写线程是否释放了写锁。
因为无论是否释放了(state回到初始值)写锁,state值都不为0,所以validate(0)永远为false
为什么上面的validate(…)方法不直接比较stamp=state,而要比较state&SBITS=state&SBITS 呢?
因为读锁和读锁是不互斥的! 所以,即使在“乐观读”的时候,state 值被修改了,但如果它改的是第7位,validate(…)还是会返回true。
另外要说明的一点是,上面使用了内存屏障VarHandle.acquireFence();,是因为在这行代码的下一行里面的stamp、SBITS变量不是volatile的,由此可以禁止其和前面的currentX=X,currentY=Y进行重排序 通过上面的分析,可以发现state的设计非常巧妙。
只通过一个变量,既实现了读锁、写锁的状态记录,还实现了数据的版本号的记录
到此这篇关于Java中的StampedLock实现原理详解的文章就介绍到这了,更多相关StampedLock实现原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
