【JUC】12.读写锁与StampedLock[完结]

红太狼 2024-04-01 10:42 119阅读 0赞

文章目录

    1. 什么是读写锁
    1. 锁的演化历程
    1. 锁降级
    1. 锁降级的策略
    1. StampedLock简介
    1. StampedLock的特点
    1. StampedLock之传统读写
    1. StampedLock之乐观锁
    1. StampedLock缺点

1. 什么是读写锁

读写锁是指ReentrantReadWriteLock类

该类能被多个读线程访问或者一个写线程访问,但是不能同时存在读写线程

其特点主要是:一体两面、读写互斥、读读共享

有线程在写的时候,其他线程不能读;有线程在读的时候,其他线程不能写


2. 锁的演化历程

第一阶段:无锁

在无锁的阶段,会导致数据大乱,多个线程写入数据,导致出现脏数据

第二阶段:synchronized 与 Lock接口(ReentrantLock类)

这两个的出现,使得线程有序,且能保持数据一致性

无论多少个线程过来,不管读还是写,每次都是一个

每次线程都是只有一个,所以会导致多个线程想读也只能一个个线程读,效率比较慢,因为读之间应该是可以共享的

第三阶段:ReadWriteLock接口(ReentrantReadWriteLock)

这个类不仅可以读写互斥,并实现了读读共享,多个线程并发可以访问,大面积可以容许多个线程来读取

在读多写少的时候,可以使用读写锁

但是也有以下缺点:

  1. 写锁饥饿问题:比如有10W个线程是读,只有一个线程是写的时候,会导致写的线程一直抢占不到资源,出现写锁饥饿问题
  2. 会出现锁降级问题

第四阶段:邮戳锁StampedLock

读的时候也允许写锁的接入(读写两个操作也让你“共享”),这样会导致我们读的数据就可能不一致,所以需要额外的方法来判断写的操作是否有写入,这是一种乐观锁

虽然乐观锁并发效率更高,但是一栏有小概率的写入导致读取的数据不一致,需要能检测出来,再读一次即可


3. 锁降级

什么是锁降级

锁降级就是ReentrantReadWriteLock将写入锁降级为读锁(就像Linux一样,写权限高于读权限),锁的严苛程度变强叫做升级,反之叫做降级

在这里插入图片描述

  1. 如果同一个线程有了写锁,在没有释放写锁的情况下,它还可以继续获取读锁。这就是写锁的降级,降级成了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序
  3. 如果释放了写锁,那么就完全转换为读锁

    public class LockDownGradingDemo {

    1. public static void main(String[] args) {
    2. ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    3. ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    4. ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    5. writeLock.lock();
    6. System.out.println("正在写入.....");
    7. readLock.lock();
    8. System.out.println("正在读.....");
    9. writeLock.unlock();
    10. readLock.unlock();
    11. }

    }

在这里插入图片描述

但是注意的是,锁可以降级,但是不可以升级,且写锁和读锁是互斥的

锁降级:遵循获取写锁–》再获取读锁–》再释放写锁的次序,写锁能够降级成为读锁

在这里插入图片描述

锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性

当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以需要释放所有读锁,才能获取写锁

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写钱锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
读锁结束,写锁有望;写锁独占,读写全堵
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁

即ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,o(一_-)o,人家还在读着那,你不先别去写,省的数据乱。


4. 锁降级的策略

为什么要锁降级呢?

在这里插入图片描述

cacheValid是一个布尔值,默认为false,如果其他线程修改过这里的数据,那么会将cacheValid改为true

上面案例是首先先读取一次数据,然后接着获取写锁,获取写锁后,判断cacheValid是否有被修改过,如果没有那将data修改为某个值,然后将cacheValid设置为true。

接着锁降级获取读锁,获取读锁之后释放写锁

对数据进行读操作

这样做的好处有以下几点:

  • 在写锁释放之前锁降级成读锁,防止其他线程竞争
  • 这样使得写完之后立马读,防止了其他线程对data进行修改从而出现脏读

5. StampedLock简介

StampedLock优化的主要是ReentrantReadWriteLock出现的锁饥饿问题

stamp代表的是锁的状态。当stamp返回零时,表示线程获取锁失败。

并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值

回顾锁饥饿问题

ReentrantReadWriteLock实现读写分离,一旦读操作比较多的时候,想要获取写锁就会变得比较困难了,假如有1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那么1个写线程就悲剧了,因此当前有可能会一直存在读锁,而无法获得写锁

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阳塞,这其实是对读锁的优化,
所以,在获取乐观读锁后,还需要对结果进行校验。


6. StampedLock的特点

  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
  3. StampedLock是不可重入锁,因为邮戳只有一个,危险(如果一个线程已经持有写锁,再去获取写的话就会导致死锁)
  4. StampedLock有三种访问模式

    1. Reading(读模式悲观),功能和ReentrantReadWriteLock的写锁类似
    2. Writing(写模式),功能和ReentrantReadWriteLock的写锁类似
    3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取的时候没人修改。假如被修改再实现升级为悲观读模式

7. StampedLock之传统读写

  1. /**
  2. * @Author: lrk
  3. * @Date: 2022/10/25 下午 8:52
  4. * @Description:
  5. */
  6. public class StampedLockDemo {
  7. static int number = 37;
  8. static StampedLock stampedLock = new StampedLock();
  9. public void write() {
  10. long stamp = stampedLock.writeLock();
  11. System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
  12. try {
  13. number += 13;
  14. } finally {
  15. stampedLock.unlockWrite(stamp);
  16. }
  17. System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备结束");
  18. }
  19. //悲观读,都没有完成的时候写锁无法获取锁
  20. public void read() {
  21. long stamp = stampedLock.readLock();
  22. System.out.println(Thread.currentThread().getName() + "\t" + "准备读,请等待.....");
  23. for (int i = 0; i < 4; i++) {
  24. try {
  25. TimeUnit.SECONDS.sleep(1);
  26. } catch (InterruptedException e) {
  27. throw new RuntimeException(e);
  28. }
  29. System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
  30. }
  31. try {
  32. int result = number;
  33. System.out.println(Thread.currentThread().getName() + "\t" + "获取成员变量值result: " + result);
  34. System.out.println("写线程没有修改成功,读锁的时候写锁无法接入,传统读写互斥");
  35. } finally {
  36. stampedLock.unlockRead(stamp);
  37. System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备结束");
  38. }
  39. }
  40. public static void main(String[] args) {
  41. StampedLockDemo resource = new StampedLockDemo();
  42. new Thread(() -> {
  43. resource.read();
  44. }, "readThread").start();
  45. try {
  46. TimeUnit.SECONDS.sleep(1);
  47. } catch (InterruptedException e) {
  48. throw new RuntimeException(e);
  49. }
  50. new Thread(() -> {
  51. resource.write();
  52. }, "writeThread").start();
  53. }
  54. }

上面案例使用的Reading和Writing两种访问模式,实现的效果与ReentrantReadWriteLock的读写锁效果一样

在这里插入图片描述


8. StampedLock之乐观锁

  1. public class StampedLockDemo {
  2. static int number = 37;
  3. static StampedLock stampedLock = new StampedLock();
  4. public void write() {
  5. long stamp = stampedLock.writeLock();
  6. System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
  7. try {
  8. number += 13;
  9. } finally {
  10. stampedLock.unlockWrite(stamp);
  11. }
  12. System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备结束");
  13. }
  14. public void read() {
  15. long stamp = stampedLock.readLock();
  16. System.out.println(Thread.currentThread().getName() + "\t" + "准备读,请等待.....");
  17. for (int i = 0; i < 4; i++) {
  18. try {
  19. TimeUnit.SECONDS.sleep(1);
  20. } catch (InterruptedException e) {
  21. throw new RuntimeException(e);
  22. }
  23. System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
  24. }
  25. try {
  26. int result = number;
  27. System.out.println(Thread.currentThread().getName() + "\t" + "获取成员变量值result: " + result);
  28. System.out.println("写线程没有修改成功,读锁的时候写锁无法接入,传统读写互斥");
  29. } finally {
  30. stampedLock.unlockRead(stamp);
  31. System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备结束");
  32. }
  33. }
  34. public void tryOptimisticRead() {
  35. long stamp = stampedLock.tryOptimisticRead();
  36. int result = number;
  37. System.out.println("4s前stampedLock.validate方法值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
  38. for (int i = 0; i < 4; i++) {
  39. try {
  40. TimeUnit.SECONDS.sleep(1);
  41. System.out.println(Thread.currentThread().getName() + "\t" + "正在读取...." + i + "秒" +
  42. stampedLock.validate(stamp) + "后stampedLock.validate方法值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
  43. } catch (InterruptedException e) {
  44. throw new RuntimeException(e);
  45. }
  46. }
  47. if (!stampedLock.validate(stamp)) {
  48. System.out.println("有人修改过--------有写操作");
  49. stamp = stampedLock.readLock();
  50. try {
  51. System.out.println("从乐观读升级为悲观读");
  52. result = number;
  53. System.out.println("重新悲观读result: " + result);
  54. } finally {
  55. stampedLock.unlockRead(stamp);
  56. }
  57. }
  58. System.out.println(Thread.currentThread().getName() + "\t" + "finally value:" + result);
  59. }
  60. public static void main(String[] args) {
  61. StampedLockDemo resource = new StampedLockDemo();
  62. new Thread(() -> {
  63. resource.tryOptimisticRead();
  64. }, "readThread").start();
  65. try {
  66. TimeUnit.SECONDS.sleep(2);
  67. } catch (InterruptedException e) {
  68. throw new RuntimeException(e);
  69. }
  70. new Thread(() -> {
  71. resource.write();
  72. }, "writeThread").start();
  73. }
  74. private static void extracted(StampedLockDemo resource) {
  75. new Thread(() -> {
  76. resource.read();
  77. }, "readThread").start();
  78. try {
  79. TimeUnit.SECONDS.sleep(1);
  80. } catch (InterruptedException e) {
  81. throw new RuntimeException(e);
  82. }
  83. new Thread(() -> {
  84. resource.write();
  85. }, "writeThread").start();
  86. }
  87. }

在这里插入图片描述


9. StampedLock缺点

  1. StampedLock不支持重入,没有Re开头
  2. StampedLock的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意
  3. 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

来源:
尚硅谷2022版JUC并发编程(对标阿里P6-P7)

发表评论

表情:
评论列表 (有 0 条评论,119人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JUC学习之

    读写锁:(并发情况下比独占锁效率要高,适用于读操作频率高,写频率低的场景) \- 写写、读写(互斥) \- 读读(不互斥) public class Test