JUC中的 StampedLock
Posted XeonYu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC中的 StampedLock相关的知识,希望对你有一定的参考价值。
上一篇:
JUC中的读写锁(ReentrantReadWriteLock)
之前我们了解了 ReentrantReadWriteLock,ReentrantReadWriteLock有一个缺点就是读锁会阻塞住写锁,这样会导致当读的线程非常多时,写锁可能会被一直阻塞,无法及时完成写入操作。
所以,在Java8中有引入了StampedLock
StampedLock
Stamped 中文是盖上邮戳的的意思,这里可以理解为给锁加了个标记,这个标记我们可以叫做版本号。
那顾名思义 StampedLock就是带戳(标记)的一把锁。
StampedLock同样支持读写锁,但是比ReentrantReadWriteLock中的读锁多了一个特性是StampedLock中的读锁可以是乐观锁
乐观锁实际上不加锁,只是判断一下版本号,不会阻塞住写锁,乐观的认为读的时候没有写的操作。所以,乐观锁在读的过程中,写锁是可以执行的。
我们先来看看StampedLock中的方法
其中很多方法都是跟ReentrantReadWriteLock中的方法类似,这里就不多说了
我们先来验证第一个点:
乐观读锁读的时候不会阻塞写操作
先来看看悲观读锁是什么样的
class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private int num = 0;
public void write() {
long stamp = stampedLock.writeLock();
try {
TimeUnit.MILLISECONDS.sleep(10);
num++;
System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockWrite(stamp);
}
}
public int read() {
long readStamp = stampedLock.readLock();
String threadName = Thread.currentThread().getName();
int readNum = 0;
try {
TimeUnit.MILLISECONDS.sleep(100);
readNum = num;
System.out.println(threadName + "读取的 readNum:" + readNum);
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockRead(readStamp);
}
return readNum;
}
}
main方法如下:
public static void main(String[] args) {
StampedLockDemo stampedLockDemo = new StampedLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(stampedLockDemo::read).start();
}
for (int i = 0; i < 10; i++) {
new Thread(stampedLockDemo::write).start();
}
}
运行结果如下
可以看到,写线程只有在读锁都释放后才会执行。
下面改成乐观锁试一下。
tryOptimisticRead就表示获取的是乐观锁。
class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private int num = 0;
public void write() {
long stamp = stampedLock.writeLock();
try {
TimeUnit.MILLISECONDS.sleep(10);
num++;
System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockWrite(stamp);
}
}
public int read() {
long readStamp = stampedLock.tryOptimisticRead();
String threadName = Thread.currentThread().getName();
int readNum = 0;
try {
TimeUnit.MILLISECONDS.sleep(100);
readNum = num;
System.out.println(threadName + "读取的 readNum:" + readNum);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
return readNum;
}
}
再来运行一下:
可以看到,读锁更改为乐观锁后,在读的期间,写操作是可以执行的。
由于 乐观读锁并不会加锁,所以,我们无需做解锁操作。
我们知道了乐观读锁读的时候,不会阻塞写操作,那么问题就来了,假如在读的过程中,恰好有其他线程执行了写操作,且写入完毕了,那么,本次读操作的数据就不对了。
我们先来看个示例:
代码如下:
class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private int num = 0;
public void write() {
long stamp = stampedLock.writeLock();
try {
TimeUnit.MILLISECONDS.sleep(10);
num++;
System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockWrite(stamp);
}
}
public int read() {
long readStamp = stampedLock.tryOptimisticRead();
int readNum = 0;
/*模拟一个线程执行写操作*/
new Thread(this::write).start();
try {
readNum = num;
/*模拟等待写线程执行完毕*/
TimeUnit.MILLISECONDS.sleep(100);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
return readNum;
}
}
main方法:
public static void main(String[] args) {
StampedLockDemo stampedLockDemo = new StampedLockDemo();
new Thread(() -> {
int readNum = stampedLockDemo.read();
System.out.println("readNum = " + readNum);
}).start();
}
运行结果:
可以看到,我们读的过程中,数据被写操作改成1了,但是,返回的依旧是0. 这个结果就不符合预期了。
所以,乐观锁读到数据后,返回数据之前一定要加个校验,判断一下读的过程中是否有写入的操作,如果有,则使用悲观读锁重新读一遍数据,以保证数据的正确性。
我们可以通过 validate 方法来校验版本号是否发生变化。
代码如下:
为了方便理解,加了很多打印以及注释
class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private int num = 0;
public void write() {
long stamp = stampedLock.writeLock();
System.out.println("写操作的版本号stamp:" + stamp);
try {
num++;
System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockWrite(stamp);
}
}
public int read() {
long readStamp = stampedLock.tryOptimisticRead();
System.out.println("第一次获取的readStamp:" + readStamp);
int readNum = 0;
try {
/*模拟一个线程执行写操作*/
Thread writeThread = new Thread(() -> {
this.write();
});
writeThread.start();
/*获取读到的值*/
readNum = num;
/*模拟等待写线程执行完毕*/
TimeUnit.MILLISECONDS.sleep(5);
/*查看线程的状态 此时线程是TERMINATED 因为乐观读锁不会阻塞住谢先成,而写线程1毫秒就执行完了*/
System.out.println("writeThread.getState() = " + writeThread.getState());
System.out.println("乐观读锁读取的数据:" + readNum);
} catch (Exception e) {
e.printStackTrace();
} finally {
}
/*返回数据之前*/
if (!stampedLock.validate(readStamp)) {
System.out.println("版本号对不上,重新获取的版本号:" + readStamp);
/*版本号对不上 表示读期间有写入操作 用悲观锁重新读一遍*/
readStamp = stampedLock.readLock();//加锁,此时会写锁会被阻塞住,等读锁完全执行完毕释放后,写操作才能执行
try {
/*再次模拟一个线程执行写操作*/
Thread writeThread = new Thread(() -> {
this.write();
});
writeThread.start();
/*重新读数据*/
readNum = num;
/*给写线程留充足的时间去执行*/
TimeUnit.MILLISECONDS.sleep(10);
/*此时写线程处于等待状态 因为悲观读锁会阻塞住写锁,写线程必须等到悲观读锁释放锁之后,才会执行*/
System.out.println("writeThread.getState() = " + writeThread.getState());
System.out.println("悲观读锁读取的数据:" + readNum);
} catch (Exception e) {
e.printStackTrace();
} finally {
/*释放锁*/
stampedLock.unlockRead(readStamp);
System.out.println("悲观读锁释放锁了,写线程可以执行了" );
}
}
return readNum;
}
}
运行结果如下:
根据运行结果看到,本次读的数据是符合预期的,在读的期间值被修改成了1,最终返回的也是1。
注意:
保证数据的正确性范围是在 本次 读操作开始到结束,本次读操作结束后你数据再更改,那就跟本次读没什么关系了,这一点不要搞混淆了。
总结
StampedLock 是ReentrantReadWriteLock的升级版,支持乐观读锁,但他是不可重入锁,主要区别在于乐观读锁实际上不上锁,不会阻塞写锁,理论上在读操作的性能上也会比ReentrantReadWriteLock的readLock性能要好(省去了加锁解锁的过程)。
如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!
以上是关于JUC中的 StampedLock的主要内容,如果未能解决你的问题,请参考以下文章