是一种基于lock的读写锁,在使用ReentrantLock时,它保证当前只有一个线程获取锁,但是有时候我们实际应用中会出现读多写少的场景,读于读之间都是读取同样的数据,如果使用ReentrantLock反而效率会低下,使用ReentrantReadWriteLock会很高效,它可以实现多个读锁同时进行,但是读与写和写于写互斥,只能有一个写锁线程在进行。
public class ReentrantReadWriteLockDemo { static class MyDemo{ // 实例化读写锁 默认非公平 private ReentrantReadWriteLock lock=new ReentrantReadWriteLock(); // 模拟共享资源 private int number; public void put(int number) { // 写锁加锁 lock.writeLock().lock(); try { Thread.sleep(500); this.number=number; System.out.println(Thread.currentThread().getName()+":写入了"+number); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 写锁释放锁 lock.writeLock().unlock(); } } public int get() { // 读写 加锁 lock.readLock().lock(); try { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":读取了"+number); return number; } finally { // 读锁解锁 lock.readLock().unlock(); } } } public static void main(String[] args) { MyDemo myDemo = new MyDemo(); // 三个写线程 for (int i = 0; i < 3; i++) { new Thread(()-> myDemo.put(new Random().nextInt(100)),"写锁"+i).start(); } // 十个读线程 for (int i = 0; i < 10; i++) { new Thread(()-> myDemo.get(),"读锁"+i).start(); } } }其中一次输出结果为:
写锁2:写入了96 读锁0:读取了96 写锁1:写入了27 读锁4:读取了27 读锁6:读取了27 读锁5:读取了27 读锁3:读取了27 读锁7:读取了27 读锁1:读取了27 读锁2:读取了27 写锁0:写入了66 读锁9:读取了66 读锁8:读取了66它是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。
StampedLock支持三种锁在一定情况下进行相互转换。例如long tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在下面几种情况下返回一个有效的stamp(也就是晋升写锁成功):
如果当前锁已经是写锁,直接返回stamp。如果当前是读锁,没有其他线程是读锁模式,返回一个写锁stamp。如果当前是乐观读锁,并且没有线程获取写锁,返回一个stamp。由于StampedLock是读写锁都是不可冲入锁,所以在获取锁后释放锁前不再调用获取锁操作,避免造成线程的阻塞,当多个线程同时尝试获取写锁和读锁,是随机性的,没有一定的规则,并且该锁不是实现Lock或ReadWriteLock接口,而是在其内部自己维护了一个双向队列。
下面一个例子是官方的提供的一个二维点的的例子:
package com.smart.home.ThreadTest; import java.util.concurrent.locks.StampedLock; class Point { // 内部定义表示坐标点 private double x, y; //定义了StampedLock锁, private final StampedLock s1 = new StampedLock(); // 写锁 public void move(double deltaX, double deltaY) { // 获得写锁 凭据 long stamp = s1.writeLock(); try { x += deltaX; y += deltaY; } finally { // 释放写锁 s1.unlockWrite(stamp); } } // 乐观锁读 public double distanceFormOrigin() { //尝试乐观读 返回stamp凭证 long stamp = s1.tryOptimisticRead(); //读取x和y的值,这时候我们并不确定x和y是否是一致的 需要下一步再次判断 double currentX = x, currentY = y; /** * 判断stamp在读过程发生期间被修改过,如果没有被修改,则这次读取有效,直接return * 如果stamp被修改过,则有可能其他线程改写了数据,会出现脏读,可以使用死循环使用乐观锁读,直到成功 * 也可以使用锁的级别,将乐观锁变为悲观锁 */ if (!s1.validate(stamp)) // 使用悲观锁读 如果有写线程那么该线程会挂起 stamp = s1.readLock(); try { currentX = x; currentY = y; } finally { // 释放读锁 s1.unlockRead(stamp); } return Math.sqrt(currentX * currentX + currentY * currentY); } // 读锁转为写锁 public void moveIfAtOrigin(double newX, double newY) { // 读锁加锁 可以使用乐观读锁替代 long stamp = s1.readLock(); try { // 如果当前是原点 则修改 while (x == 0.0 && y == 0.0) { // 尝试升级为写锁 long ws = s1.tryConvertToWriteLock(stamp); // 升级成功 更新stamp凭据 设置坐标值 退出循环 if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { // 升级失败 释放读锁,重新获取写锁,循环重试 s1.unlockRead(stamp); stamp = s1.writeLock(); } } } finally { // 释放锁 s1.unlock(stamp); } } }使用乐观锁读可以避免写锁处于饥饿状态,增加吞吐量,但是使用乐观锁读也是很容易犯错误的,在使用上必须保证以下顺序。
//乐观读 返回stamp凭证 long stamp = lock.tryOptimisticRead(); // 复制变量到方法栈 CopyVariablesMethodStack(); // 校验stamp凭证 if (!lock.validate(stamp)){ // 获取读锁 stamp = lock.readLock(); try { // 复制变量到方法栈 CopyVariablesMethodStack(); } finally { // 释放读锁 lock.unlockRead(stamp); } } // 操作操作复制到方法栈中的变量 ManipulateVariablesCopeMethodStack();