Java并发编程七ReentrantReadWriteLock和StampedLock

    xiaoxiao2025-01-12  15

    ReentrantReadWriteLock和StampedLock

    ReentrantReadWriteLockStampedLock三种锁注意点案例说明 Java并发编程一:并发基础必知 Java并发编程二:Java中线程 Java并发编程三:volatile使用 Java并发编程四:synchronized和lock Java并发编程五:Atomic原子类 Java并发编程六:并发队列

    ReentrantReadWriteLock

    是一种基于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

    StampedLock

    它是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。

    三种锁

    写锁writeLock: 是一个独占锁写锁,当一个线程获得该锁后,其他请求读锁或者写锁的线程阻塞, 获取成功后,会返回一个stamp(凭据)变量来表示该锁的版本,在释放锁时调用unlockWrite方法传递stamp参数。提供了非阻塞式获取锁tryWriteLock。悲观读锁readLock: 是一个共享读锁,在没有线程获取写锁情况下,多个线程可以获取该锁。如果有写锁获取,那么其他线程请求读锁会被阻塞。悲观读锁会认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据进行加锁,这是在读少写多的情况下考虑的。请求该锁成功后会返回一个stamp值,在释放锁时调用unlockRead方法传递stamp参数。提供了非阻塞式获取锁方法tryWriteLock。乐观读锁tryOptimisticRead: 相对比悲观读锁,在操作数据前并没有通过CAS设置锁的状态,如果没有线程获取写锁,则返回一个非0的stamp变量,获取该stamp后在操作数据前还需要调用validate方法来判断期间是否有线程获取了写锁,如果是返回值为0则有线程获取写锁,如果不是0则可以使用stamp变量的锁来操作数据。由于tryOptimisticRead并没有修改锁状态,所以不需要释放锁。这是读多写少的情况下考虑的,不涉及CAS操作,所以效率较高,在保证数据一致性上需要复制一份要操作的变量到方法栈中,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性得到了保证。

    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();
    最新回复(0)