Java并发编程--锁原理之StampedLock锁

曾经终败给现在 2023-07-01 15:53 45阅读 0赞

文章目录

  • JDK 8 中新增的StampedLock锁的原理
      • (1). 概述
      • (2). 写锁writeLock
      • (3). 悲观读readLock
      • (4). 乐观读OptimisticRead()
      • (5). 锁转换
      • (6). 使用乐观读锁

JDK 8 中新增的StampedLock锁的原理

(1). 概述

 StampedLock锁是并发包中JDK 8版本新增的一个锁,提供了三种模式的读写控制.当调用try系列方法尝试获取锁时,会返回一个long型的变量stamp(戳记).这个戳记代表了锁的状态,可以理解为乐观锁中的版本.释放锁的转换锁时都需要提供这个标记来判断权限.

特点:

  • 所有获取锁的方法都会返回一个stamp戳记,0为失败,其他为获取成功
  • 所有释放锁的方法都需要一个stamp戳记,必须和之前得到锁时的stamp一致
  • 不可重入
  • 有三种访问模式

    • 乐观读
    • 悲观读
    • 写锁
  • 写锁可以降级为读锁
  • 都不支持条件队列

    /* 等待链表队列,每一个WNode标识一个等待线程 /
    static final class WNode {

    1. volatile WNode prev;
    2. volatile WNode next;
    3. volatile WNode cowait; // 读模式使用该节点形成栈
    4. volatile Thread thread; // non-null while possibly parked
    5. volatile int status; // 0, WAITING, or CANCELLED
    6. final int mode; // RMODE or WMODE
    7. WNode(int m, WNode p) {
    8. mode = m; prev = p; }

    }

  1. /** 锁队列状态,当处于写模式时第8位为1,读模式时前7为为1-126(读锁是共享锁,附加的readerOverflow用于当读者超过126时) */
  2. private transient volatile long state;
  3. /** 将state超过 RFULL=126的值放到readerOverflow字段中 */
  4. private transient int readerOverflow;

(2). 写锁writeLock

独占锁,不可重入.请求该锁成功后会返回一个stamp戳记变量来表示该锁的版本.

  • writeLock():当完全没有加锁时,绕过acquireWrite,否则调用acquireWrite入队列获取锁资源,

    • acquireWrite():入队自旋,并放到队列尾部,如果队列中只剩下一个结点,则在队头进一步自旋,最后会进入阻塞
  • unlockWrite():如果锁的状态与stamp相同,调用release()释放锁

    • release():唤醒传入节点的后继节点

(3). 悲观读readLock

共享锁,不可重入

  • readLock():(队列为空&&没有写锁同时读锁数小于126&&CAS修改状态成功)则状态加1并返回,否则调用acquireRead()自旋获取读锁

    • acquireRead():首先是入队自旋,如果队尾不是读模式则放到队列尾部,如果是读模式,则放到队尾的cowait中。如果队列中只剩下一个结点,则在队头进一步自旋.如果最终依然失败,则Unsafe().park()挂起当前线程。
  • unlockRead():如果state匹配stamp,判断当前的共享次数,修改state或者readerOverflow

(4). 乐观读OptimisticRead()

 在操作数据前并没有通过 CAS 设置锁的状态,仅仅是通过位运算测试

 如果当前没有线程持有写锁,则简单的返回一个非 0 的 stamp 版本信息,获取该 stamp 后在具体操作数据前还需要调用 validate 验证下该 stamp 是否已经不可用,也就是看当调用 tryOptimisticRead 返回 stamp 后,到当前时间是否有其它线程持有了写锁,如果是那么 validate 会返回 0,否则就可以使用该 stamp 版本的锁对数据进行操作。

 由于 tryOptimisticRead 并没有使用 CAS 设置锁状态,所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及 CAS 操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其它写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。

  1. /**
  2. * 获取乐观读锁,返回stamp
  3. */
  4. public long tryOptimisticRead() {
  5. long s; //有写锁返回0,否则返回256
  6. return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
  7. }
  8. /**
  9. * 验证从调用tryOptimisticRead开始到现在这段时间内有无写锁占用过锁资源,有写锁获得过锁资源则返回false. stamp为0返回false.
  10. * @return 从返回stamp开始,没有写锁获得过锁资源返回true,否则返回false
  11. */
  12. public boolean validate(long stamp) {
  13. //强制读取操作和验证操作在一些情况下的内存排序问题
  14. U.loadFence();
  15. //当持有写锁后再释放写锁,该校验也不成立,返回false
  16. return (stamp & SBITS) == (state & SBITS);
  17. }

(5). 锁转换

  1. /**
  2. * state匹配stamp时, 执行下列操作之一.
  3. * 1、stamp 已经持有写锁,直接返回.
  4. * 2、读模式,但是没有更多的读取者,并返回一个写锁stamp.
  5. * 3、有一个乐观读锁,只在即时可用的前提下返回一个写锁stamp
  6. * 4、其他情况都返回0
  7. */
  8. public long tryConvertToWriteLock(long stamp) {
  9. long a = stamp & ABITS, m, s, next;
  10. //state匹配stamp
  11. while (((s = state) & SBITS) == (stamp & SBITS)) {
  12. //没有锁
  13. if ((m = s & ABITS) == 0L) {
  14. if (a != 0L)
  15. break;
  16. //CAS修改状态为持有写锁,并返回
  17. if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
  18. return next;
  19. }
  20. //持有写锁
  21. else if (m == WBIT) {
  22. if (a != m)
  23. //其他线程持有写锁
  24. break;
  25. //当前线程已经持有写锁
  26. return stamp;
  27. }
  28. //有一个读锁
  29. else if (m == RUNIT && a != 0L) {
  30. //释放读锁,并尝试持有写锁
  31. if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT))
  32. return next;
  33. }else
  34. break;
  35. }
  36. return 0L;
  37. }
  38. /**
  39. * state匹配stamp时, 执行下列操作之一.
  40. 1、stamp 表示持有写锁,释放写锁,并持有读锁
  41. 2 stamp 表示持有读锁 ,返回该读锁
  42. 3 有一个乐观读锁,只在即时可用的前提下返回一个读锁stamp
  43. 4、其他情况都返回0,表示失败
  44. *
  45. */
  46. public long tryConvertToReadLock(long stamp) {
  47. long a = stamp & ABITS, m, s, next; WNode h;
  48. //state匹配stamp
  49. while (((s = state) & SBITS) == (stamp & SBITS)) {
  50. //没有锁
  51. if ((m = s & ABITS) == 0L) {
  52. if (a != 0L)
  53. break;
  54. else if (m < RFULL) {
  55. if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
  56. return next;
  57. }
  58. else if ((next = tryIncReaderOverflow(s)) != 0L)
  59. return next;
  60. }
  61. //写锁
  62. else if (m == WBIT) {
  63. //非当前线程持有写锁
  64. if (a != m)
  65. break;
  66. //释放写锁持有读锁
  67. state = next = s + (WBIT + RUNIT);
  68. if ((h = whead) != null && h.status != 0)
  69. release(h);
  70. return next;
  71. }
  72. //持有读锁
  73. else if (a != 0L && a < WBIT)
  74. return stamp;
  75. else
  76. break;
  77. }
  78. return 0L;
  79. }

(6). 使用乐观读锁

 使用乐观读要保证以下顺序:

  1. // 获取版本信息(乐观锁)
  2. long stamp = lock.tryOptimisticRead();
  3. // 复制变量到本地堆栈
  4. copyVaraibale2ThreadMemory();
  5. // 校验,如果校验失败,说明此处乐观锁使用失败,申请悲观读锁
  6. if(!lock.validate(stamp)){
  7. // 申请悲观读锁
  8. long stamp = lock.readLock();
  9. try{
  10. // 复制变量到本地堆栈
  11. copyVaraibale2ThreadMemory();
  12. }finally{
  13. // 使用完之后释放锁
  14. lock.unlock();
  15. }
  16. }

最后用一幅图加深理解

在这里插入图片描述

发表评论

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

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

相关阅读