JUC 十二. ReentrantReadWriteLock 与 StampedLock

一. 基础

  1. ReentrantReadWriteLock 可以看为读读共享,读写,写写依然互斥,总结一句话: 读写互斥,读读共享,既一个资源可以被多个读操作或一个写操作访问
  2. ReentrantReadWriteLock 降级策略: 首先同一时间内允许多个读,但是读锁过程中,要获取写锁,当前正持有读锁的线程必须释放,否则无法获取,写锁时,允许插入读锁进来读取
  3. 先总结ReentrantReadWriteLock 的优点: 一. 允许多个读同时执行,在读多写少的需求中效率更高,二.根据该锁的降级策略,在获取写锁后再不释放写锁情况下允许获取读锁,增加数据可见性(也就是获取写锁修改数据成功后,防止写锁不释放,读请求进来不能及时拿到修改完成的数据)
  4. 使用示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDemo {
    //1.声明读写锁
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //2.共享数据
    private String data = "";

    //3.读方法
    public void read() {
        //1.获取读锁
        readWriteLock.readLock().lock();
        System.out.println("读取数据 data:" + data);
        //2.释放读锁
        readWriteLock.readLock().unlock();
    }

    //4.写方法
    private void write(String value) {
        //1.获取写锁
        readWriteLock.writeLock().lock();
        this.data = value;
        //2.释放写锁
        readWriteLock.writeLock().unlock();
    }

    //运行测试,开启两个线程,一个线程写,一下线程读
    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
        new Thread(() -> {
            demo.write("aaaa");
        }, "t1").start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            demo.read();
        }, "t1").start();

        TimeUnit.SECONDS.sleep(5);
        System.out.println("主线程最终读取数据 data:" + demo.data);
    }
}

二. ReentrantReadWriteLock 的锁降级

  1. 此处的锁降级是指: 将写锁可以降级为读锁,也就是一个线程通过ReentrantReadWriteLock 获取到了写锁,在没有释放写锁的情况下,还可以获取到读锁
  2. 提供锁降级的原因是: 让线程感知到数据的变化,保证数据可见性
  3. 示例代码,在下方test方法中先获取写锁,然后再不释放写锁的情况下是可以获取读锁的
	public void test(){
		//1.声明读写两把锁
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        
        //2.获取写锁
        writeLock.lock();
        System.out.println("获取到写锁");

		//3.在不释放写锁的情况下,获取读锁
        readLock.lock();
        System.out.println("获取到读锁");
        
        //4.释放锁
        writeLock.unlock();
        
        readLock.unlock();
    }
  1. 总结:由ReentrantReadWriteLock 锁降级中可以理解到该锁除了读读共存以外,在不释放写锁的情况下也可以获取读锁继续执行,写读也是共存,但是在读锁没有释放时,无法获取写锁,所以写写互斥,读写也互斥

三. StampedLock 邮戳票据锁

  1. 思考一下 ReentrantReadWriteLock 存在的问题:
  1. 锁饥饿问题,假设当前有99个读请求进来,后续又进来一个写请求,但是需求上要求,只要有修改,马上生效,在使用 ReentrantReadWriteLock,极端情况下可能要等到前面99个读请求执行完毕,这个写请求才会有机会执行,例如一个购物系统,站在商户角度,修改完价格就想马上生效,但是由于高并发前面拍着多个购买的读请求,就会造成这些阻塞购买读请求执行完后,这个修改价格的写请求才能生效
  2. 虽然 ReentrantReadWriteLock 提供了降级策略,获取写锁,在未释放的情况下允许获取读锁,可以提高数据可见性,但是当先获取读锁,在读锁不能释放的情况下不能获取写锁,必须要等到前面的读锁先释放才行
  3. 根据上面两个问题出现了StampedLock 邮戳票据锁
  1. StampedLock 邮戳票据锁是什么: 是java8中根据 ReentrantReadWriteLock 优化升级新增的锁,内部通过一个long类型的stamp标记实现的比ReentrantReadWriteLock 性能更高的锁(stamp表示锁的状态,为0时表示线程获取锁失败,并且释放转换锁类型时需要传递最初获取的stamp的值)
  2. 先说一下 StampedLock 邮戳票据锁的特点:
  1. 在所有调用获取锁方法都会返回一个戳记stamp,如果为0表示获取锁失败,其它表示获取锁成功
  2. 在所有调用释放锁方法都需要传入一个戳记stamp,并且传入的这个戳记要与获取锁时的戳记保持一致
  3. 该锁是不可重入锁,如果写锁已经被获取,再去获取写锁会出现死锁问题
  1. StampedLock 支持三种访问模式
  1. Reading 读模式: 功能与 ReentrantReadWriteLock 类似获取一个读锁
  2. Writing 写模式: 功能与 ReentrantReadWriteLock 类似获取一个写锁
  3. Optimistic reading 乐观读模式: 无锁机制,类似于数据库的乐观锁,支持读写并发,根据stamp判断,如果发现被修改回升级为悲观读
  1. 代码示例(重点关注乐观读,通过stamp验证乐观读锁获取未释放前中间是否有写锁被获取,解决了ReentrantReadWriteLock 中读锁必须被释放才能获取读锁的问题,读写并不会阻塞解决了线饥饿问题,读请求可以马上感知到)
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {

    //1.声明StampedLock锁
    private StampedLock stampedLock = new StampedLock();

    //2.共享数据
    private String data = "";

    //写方法
    public void write(String value) {
        //1.获取写锁.返回 stamp 值,如果为0说明获取失败
        long stamp = stampedLock.writeLock();
        try {
            //2.执行业务逻辑
            this.data = value;
        } catch (Exception e) {

        } finally {
            //3.释放写锁,需要传入前面获取锁时返回的stamp戳记,
            stampedLock.unlockWrite(stamp);
        }
    }

    //3.读方法,StampedLock中通过 readLock()获取的读锁与 ReentrantReadWriteLock
    //相同,在当前读锁未释放情况下不可获取写锁
    public void read() {
        //1.获取读锁,返回stamp值,如果为0说明获取锁失败
        long stamp = stampedLock.readLock();
        System.out.println(this.data);
        //2.释放读锁,需要传入获取锁时返回的stamp
        stampedLock.unlockRead(stamp);
    }

    //4.乐观读,通过tryOptimisticRead()获取到读锁时,如果当前读锁未释放
    //允许获取写锁,可能出现数据不安全问题,可以通过stamp作为版本号验证
    //当前读时是否有写锁被获取,如果有被获取防止数据被篡改拿到脏数据
    //在重新读一次数据
    public void tryOptimisticRead() {
        //1.获取乐观读锁,并返回戳记
        long stamp = stampedLock.tryOptimisticRead();

        System.out.println("第一次读取数据 data:" + data);

        //2.通过stamp验证是否有写锁被获取,防止写请求进来修改数据,没有返回true
        boolean flag = stampedLock.validate(stamp);
        if (!flag) {
            //3.当使用乐观读锁,获取读锁未释放时,中间有
            //写锁被获取,当前乐观读锁要切换到普通读锁模式
            //重新获取一个普通读锁,并返回新的戳记stamp
            stamp = stampedLock.readLock();
            System.out.println("有写锁被获取,重新读取 data:" + data);

            //4.释放锁(乐观读锁不需要释放?)
            stampedLock.unlockRead(stamp);
        }
    }

    //运行测试
    public static void main(String[] args) {
        StampedLockDemo demo = new StampedLockDemo();
        //1.写数据
        demo.write("aaaaa");

        //2.读数据
        demo.read();

        //3.乐观读
        demo.tryOptimisticRead();
    }
}
  1. StampedLock邮戳标记锁的缺点: (如果感觉处理不好不推荐使用)
  1. 不支持重入,假设一个线程第一次获取到锁,再次执行该方法,会造成死锁
  2. 普通读锁与普通写锁模式不支持条件变量
  3. 使用该锁时一定不能中断线程操作,例如执行interrupt(),会出现异常
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值