JDK 并发编程 - 读写锁的改进:StampedLock 时间: 2018-09-28 18:19 分类: JDK,JAVA 前面一片文章介绍过了`ReadWriteLock`读写锁的使用,虽然读写锁分离了读和写的功能,使得读与读之间可以完全并发,但是读与写之间还是冲突的。 读锁会完全阻塞写锁,它使用的依然是悲观的锁策略,如果有大量的读线程,它就有可能引起写线程的`饥饿(即长时间获取不到写锁,更新不了数据)`。 所以在`JDK1.8`中引入了一种新的锁机制:`StampedLock`。它提供了一种乐观的读策略,使得读线程可以完全不会阻塞写线程。 下面是 JDK 文档中的例子: ```java public class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) { // 排它锁方法 long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } } double distanceFromOrigin() { // 乐观读 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); } void moveIfAtOrigin(double newX, double newY) { // 悲观读 // 疑惑?一开始就使用悲观读锁,下面的tryConvertToWriteLock锁转换还会成功吗? // tryConvertToWriteLock 当参数 stamp 为读锁返回的时候,如果写锁可用,则将读锁升级为写锁并返回写锁的 stamp // readLock 一旦获取悲观读锁成功,就不可能有其他线程获取到写锁,那么写锁始终是不可用的 tryConvertToWriteLock 这个转换还会成功吗? long stamp = sl.readLock(); try { while (x == 0.0 && y == 0.0) { long ws = sl.tryConvertToWriteLock(stamp); //转换为写锁 if (ws != 0L) { //转换成功 stamp = ws; //替换票据 x = newX; y = newY; break; } else { sl.unlockRead(stamp); //转换失败,先释放读锁 stamp = sl.writeLock(); //再直接申请悲观写锁 } } } finally { sl.unlock(stamp); } } } ``` 代码中的`distanceFromOrigin`方法使用的是乐观读,`tryOptimisticRead`会返回一个票据,`validate`方法根据传入的`stamp`判断在读过程中是否发生过修改,在上面代码中如果发现`stamp`被修改过,就使用`readLock`升级为悲观读,当然我们也可以使用类似`CAS`操作用一个死循环来一直乐观读,直到成功为止。 下面的`moveIfAtOrigin`悲观读方法,疑惑在注释中一直想不明白,知道的朋友帮忙解答下吧,查了很多文章都是说转换为写锁一笔带过,而没有去考虑整个方法到底起个什么作用,下面简单说下自己的理解: > **既然需要修改数据,为什么不直接使用悲观写锁,而是一开始去获取一个悲观读锁?** 因为由方法名`moveIfAtOrigin`可以知道,该方法是说如果当前点是原点的话就进行移动,也就是说当前点可能不是原点,那么如果我们直接使用悲观写锁的话,当前点不是原点,仅仅是为了读取这个点的位置就使用了悲观写锁,而写锁是会阻塞读锁的,所以造成浪费,尤其是当前点不在原点的概率比较大时,如果直接使用写锁的话性能会明显下降。而`StampedLock`和`ReadWriteLock`读写锁一样:`读读之间是不会阻塞的`,所以先使用读锁来判断当前点是否为原点。 > **当获取到悲观读锁,读到当前点是原点的时候,尝试将读锁转换为写锁,转换过程中之前的悲观读锁会不会释放?** `tryConvertToWriteLock`方法官方文档解释是,当传入的参数 stamp 是读锁的票据时,如果写锁可用,就释放读锁,返回写锁的 stamp 票据,注意:到底是先判断写锁是否可用,然后再释放读锁?还是先释放读锁,然后去竞争写锁?这点就是我没想明白的地方,如果是前者,读锁没释放之前,因为读锁和写锁之间是排斥的,下面的 Demo 程序可以验证(在`读锁`未释放之前,`写锁`是获取不到的),那么写锁肯定是不可用的,`tryConvertToWriteLock`必定返回 0 失败,然后执行代码中的 ```java sl.unlockRead(stamp); //转换失败,先释放读锁 stamp = sl.writeLock(); //再直接申请悲观写锁 ``` 那么上面官方例子中的`tryConvertToWriteLock`操作还有什么意义?不如直接去申请写锁。 后面一种情况就是`tryConvertToWriteLock`会先释放读锁然后去竞争写锁,因为是先释放的读锁,所以就有可能竞争成功。 **读写锁验证 Demo** ```java import java.util.Random; import java.util.concurrent.locks.StampedLock; public class StampedLockTest { static int x = 0; static StampedLock sl = new StampedLock(); static int read() throws InterruptedException { long stamp = sl.readLock(); try { System.out.println("x 开始读取"); Thread.sleep(5000); System.out.println("x 读取完毕"); return x; } finally { sl.unlock(stamp); } } static void write(int n) { long stamp = sl.writeLock(); try { x = n; System.out.println("x 被修改了"); } finally { sl.unlock(stamp); } } public static void main(String[] args) { try { //读线程 Thread t1 = new Thread(() -> { try { read(); } catch (InterruptedException e) { e.printStackTrace(); } }); //写线程 Thread t2 = new Thread(() -> write(100)); t1.start(); Thread.sleep(1000); t2.start(); t1.join(); t2.join(); System.out.println(x); } catch (InterruptedException e) { e.printStackTrace(); } } } ``` 运行上面的代码可以发现写操作总是在读操作之后执行,也就是说读写锁之间是互斥的,在申请到读锁的时候,在没有释放读锁之前,写锁始终是不可用的。 以上纯属个人理解,如果有误,还请赐教。 标签: 并发锁