zoukankan      html  css  js  c++  java
  • java 并发(五)---AbstractQueuedSynchronizer(4)

    问题 :

    • rwl 的底层实现是什么,应用场景是什么

    读写锁 ReentrantReadWriteLock

              首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 ReentranLock 有什么区别?Reentrancy 英文的意思是可重入性。ReentrantReadWriteLock下文简称(rrwl)

            下面总结来自   Java并发编程—ReentrantReadWriteLock ,你也可以从JDK 中阅读到这段。

            ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

            ReentrantReadWriteLock支持以下功能:

        1)支持公平和非公平的获取锁的方式;

        2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;

        3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;(这也说明了在rrwl中写锁的等级高于读锁)

        4)读取锁和写入锁都支持锁获取期间的中断;

        5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。 

                 

               下面是两个来自文档的Demo

      1 class CachedData {
      2     Object data;
      3     volatile boolean cacheValid;
      4     final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
      5 
      6     void processCachedData() {
      7       rwl.readLock().lock();
      8       if (!cacheValid) {
      9         // Must release read lock before acquiring write lock
     10         rwl.readLock().unlock();
     11         rwl.writeLock().lock();
     12         try {
     13           // Recheck state because another thread might have
     14           // acquired write lock and changed state before we did.
     15           if (!cacheValid) {
     16             data = ...
     17             cacheValid = true;
     18           }
     19           // Downgrade by acquiring read lock before releasing write lock
     20           rwl.readLock().lock();
     21         } finally {
     22           rwl.writeLock().unlock(); // Unlock write, still hold read
     23         }
     24       }
     25 
     26       try {
     27         use(data);
     28       } finally {
     29         rwl.readLock().unlock();
     30       }
     31     }
     32   }

                在一些集合的使用上,rrwl 可以用来提升并发性。

      1 class RWDictionary {
      2     private final Map<String, Data> m = new TreeMap<String, Data>();
      3     private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
      4     private final Lock r = rwl.readLock();
      5     private final Lock w = rwl.writeLock();
      6 
      7     public Data get(String key) {
      8       r.lock();
      9       try { return m.get(key); }
     10       finally { r.unlock(); }
     11     }
     12     public String[] allKeys() {
     13       r.lock();
     14       try { return m.keySet().toArray(); }
     15       finally { r.unlock(); }
     16     }
     17     public Data put(String key, Data value) {
     18       w.lock();
     19       try { return m.put(key, value); }
     20       finally { w.unlock(); }
     21     }
     22     public void clear() {
     23       w.lock();
     24       try { m.clear(); }
     25       finally { w.unlock(); }
     26     }
     27   }

    ReentrantReadWriteLock 概述

           我们之前分析AQS 知道了,在AQS中使用的两种模式,一种是独占模式,一种是共享模式。

    13

    (图片来源见参考资料)

    AQS 的精髓在于内部的属性 state

    1. 对于独占模式来说,通常就是 0 代表可获取锁,1 代表锁被别人获取了,重入例外
    2. 而共享模式下,每个线程都可以对 state 进行加减操作

    也就是说,独占模式和共享模式对于 state 的操作完全不一样,那读写锁 ReentrantReadWriteLock 中是怎么使用 state 的呢?答案是将 state 这个 32 位的 int 值分为高 16 位和低 16位,分别用于共享模式和独占模式

    ReentrantReadWriteLock 源码分析

         下文部分源码分析来自参考资料,摘抄过来只是为了纪录。

      1 abstract static class Sync extends AbstractQueuedSynchronizer {
      2     // 下面这块说的就是将 state 一分为二,高 16 位用于共享模式,低16位用于独占模式
      3     static final int SHARED_SHIFT   = 16;
      4     static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
      5     static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
      6     static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
      7     // 取 c 的高 16 位值,代表读锁的获取次数(包括重入)
      8     static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
      9     // 取 c 的低 16 位值,代表写锁的重入次数,因为写锁是独占模式
     10     static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
     11 
     12     // 这个嵌套类的实例用来记录每个线程持有的读锁数量(读锁重入)
     13     static final class HoldCounter {
     14         // 持有的读锁数
     15         int count = 0;
     16         // 线程 id
     17         final long tid = getThreadId(Thread.currentThread());
     18     }
     19 
     20     // ThreadLocal 的子类
     21     static final class ThreadLocalHoldCounter
     22         extends ThreadLocal<HoldCounter> {
     23         public HoldCounter initialValue() {
     24             return new HoldCounter();
     25         }
     26     }
     27     /**
     28       * 组合使用上面两个类,用一个 ThreadLocal 来记录当前线程持有的读锁数量
     29       */
     30     private transient ThreadLocalHoldCounter readHolds;
     31 
     32     // 用于缓存,记录"最后一个获取读锁的线程"的读锁重入次数,
     33     // 所以不管哪个线程获取到读锁后,就把这个值占为已用,这样就不用到 ThreadLocal 中查询 map 了
     34     // 算不上理论的依据:通常读锁的获取很快就会伴随着释放,
     35     //   显然,在 获取->释放 读锁这段时间,如果没有其他线程获取读锁的话,此缓存就能帮助提高性能
     36     private transient HoldCounter cachedHoldCounter;
     37 
     38     // 第一个获取读锁的线程(并且其未释放读锁),以及它持有的读锁数量
     39     private transient Thread firstReader = null;
     40     private transient int firstReaderHoldCount;
     41 
     42     Sync() {
     43         // 初始化 readHolds 这个 ThreadLocal 属性
     44         readHolds = new ThreadLocalHoldCounter();
     45         // 为了保证 readHolds 的内存可见性
     46         setState(getState()); // ensures visibility of readHolds
     47     }
     48     ...
     49 }

        

            注意一下,里面有个HoldCounter 是静态内部类,所以外部只要引用到它就会改变到 readHolds 的值。在读锁获取锁的时候会讲到。

    • readHolds :  某个线程获取读锁的数量,用来记录某个线程的重入锁数量。他是数据结构是ThreadLocalHoldCounter ,而ThreadLocalHoldCounter 是继承 ThreadLocal 。
    • cachedHoldCounter : 可以从名字看出来是缓存的作用,它代表的是最后一个线程成功获取读锁的数量。它可以起到缓存的作用的依据是刚释放的锁又会很快地去获取锁。

    读锁获取

      1 // ReadLock
      2 public void lock() {
      3     sync.acquireShared(1);
      4 }
      5 // AQS
      6 public final void acquireShared(int arg) {
      7     if (tryAcquireShared(arg) < 0)
      8         doAcquireShared(arg);
      9 }
      1 protected final int tryAcquireShared(int unused) {
      2 
      3     Thread current = Thread.currentThread();
      4     int c = getState();
      5 
      6     // exclusiveCount(c) 不等于 0,说明有线程持有写锁,
      7     //    而且不是当前线程持有写锁,那么当前线程获取读锁失败
      8     //         (另,如果持有写锁的是当前线程,是可以继续获取读锁的)
      9     if (exclusiveCount(c) != 0 &&
     10         getExclusiveOwnerThread() != current)
     11         return -1;
     12 
     13     // 读锁的获取次数
     14     int r = sharedCount(c);
     15 
     16     // 读锁获取是否需要被阻塞,稍后细说。为了进去下面的分支,假设这里不阻塞就好了
     17     if (!readerShouldBlock() &&
     18         // 判断是否会溢出 (2^16-1,没那么容易溢出的)
     19         r < MAX_COUNT &&
     20         // 下面这行 CAS 是将 state 属性的高 16 位加 1,低 16 位不变,如果成功就代表获取到了读锁
     21         compareAndSetState(c, c + SHARED_UNIT)) {
     22 
     23         // =======================
     24         //   进到这里就是获取到了读锁
     25         // =======================
     26 
     27         if (r == 0) {
     28             // r == 0 说明此线程是第一个获取读锁的,或者说在它前面获取读锁的都走光光了,它也算是第一个吧
     29             //  记录 firstReader 为当前线程,及其持有的读锁数量:1
     30             firstReader = current;
     31             firstReaderHoldCount = 1;
     32         } else if (firstReader == current) {
     33             // 进来这里,说明是 firstReader 重入获取读锁(这非常简单,count 加 1 结束)
     34             firstReaderHoldCount++;
     35         } else {
     36             // 前面我们说了 cachedHoldCounter 用于缓存最后一个获取读锁的线程
     37             // 如果 cachedHoldCounter 缓存的不是当前线程,设置为缓存当前线程的 HoldCounter
     38             HoldCounter rh = cachedHoldCounter;
     39             if (rh == null || rh.tid != getThreadId(current))
     40                 cachedHoldCounter = rh = readHolds.get();
     41             else if (rh.count == 0)
     42                 // 到这里,那么就是 cachedHoldCounter 缓存的是当前线程,但是 count 为 0,
     43                 // 大家可以思考一下:这里为什么要 set ThreadLocal 呢?(当然,答案肯定不在这块代码中)
     44                 //   既然 cachedHoldCounter 缓存的是当前线程,
     45                 //   当前线程肯定调用过 readHolds.get() 进行初始化 ThreadLocal
     46                 readHolds.set(rh);
     47 
     48             // count 加 1
     49             rh.count++;
     50         }
     51         // return 大于 0 的数,代表获取到了共享锁
     52         return 1;
     53     }
     54     // 往下看
     55     return fullTryAcquireShared(current);
     56 }
     

             上面的代码中,要进入 if 分支,需要满足:readerShouldBlock() 返回 false,并且 CAS 要成功(我们先不要纠结 MAX_COUNT 溢出。 那我们反向推,怎么样进入到最后的 fullTryAcquireShared :

    • readerShouldBlock() 返回 true,2 种情况:

      • 在 FairSync 中说的是 hasQueuedPredecessors(),即阻塞队列中有其他元素在等待锁。

        也就是说,公平模式下,有人在排队呢,你新来的不能直接获取锁

      • 在 NonFairSync 中说的是 apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。

        作者给写锁定义了更高的优先级,所以如果碰上获取写锁的线程马上就要获取到锁了,获取读锁的线程不应该和它抢。

        如果 head.next 不是来获取写锁的,那么可以随便抢,因为是非公平模式,大家比比 CAS 速度

    • compareAndSetState(c, c + SHARED_UNIT) 这里 CAS 失败,存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。

             然后就会来到 fullTryAcquireShared 中再次尝试:

      1 /**
      2  * 1. 刚刚我们说了可能是因为 CAS 失败,如果就此返回,那么就要进入到阻塞队列了,
      3  *    想想有点不甘心,因为都已经满足了 !readerShouldBlock(),也就是说本来可以不用到阻塞队列的,
      4  *    所以进到这个方法其实是增加 CAS 成功的机会
      5  * 2. 在 NonFairSync 情况下,虽然 head.next 是获取写锁的,我知道它等待很久了,我没想和它抢,
      6  *    可是如果我是来重入读锁的,那么只能表示对不起了
      7  */
      8 final int fullTryAcquireShared(Thread current) {
      9     HoldCounter rh = null;
     10     // 别忘了这外层有个 for 循环
     11     for (;;) {
     12         int c = getState();
     13         // 如果其他线程持有了写锁,自然这次是获取不到读锁了,乖乖到阻塞队列排队吧
     14         if (exclusiveCount(c) != 0) {
     15             if (getExclusiveOwnerThread() != current)
     16                 return -1;
     17             // else we hold the exclusive lock; blocking here
     18             // would cause deadlock.
     19         } else if (readerShouldBlock()) {
     20             /**
     21               * 进来这里,说明:
     22               *  1. exclusiveCount(c) == 0:写锁没有被占用
     23               *  2. readerShouldBlock() 为 true,说明阻塞队列中有其他线程在等待
     24               *
     25               * 既然 should block,那进来这里是干什么的呢?
     26               * 答案:是进来处理读锁重入的!
     27               *
     28               */
     29 
     30             // firstReader 线程重入读锁,直接到下面的 CAS
     31             if (firstReader == current) {
     32                 // assert firstReaderHoldCount > 0;
     33             } else {
     34                 if (rh == null) {
     35                     rh = cachedHoldCounter;
     36                     if (rh == null || rh.tid != getThreadId(current)) {
     37                         // cachedHoldCounter 缓存的不是当前线程
     38                         // 那么到 ThreadLocal 中获取当前线程的 HoldCounter
     39                         // 如果当前线程从来没有初始化过 ThreadLocal 中的值,get() 会执行初始化
     40                         rh = readHolds.get();
     41                         // 如果发现 count == 0,也就是说,纯属上一行代码初始化的,那么执行 remove
     42                         // 然后往下两三行,乖乖排队去
     43                         if (rh.count == 0)
     44                             readHolds.remove();
     45                     }
     46                 }
     47                 if (rh.count == 0)
     48                     // 排队去。
     49                     return -1;
     50             }
     51             /**
     52               * 这块代码我看了蛮久才把握好它是干嘛的,原来只需要知道,它是处理重入的就可以了。
     53               * 就是为了确保读锁重入操作能成功,而不是被塞到阻塞队列中等待
     54               *
     55               * 另一个信息就是,这里对于 ThreadLocal 变量 readHolds 的处理:
     56               *    如果 get() 后发现 count == 0,居然会做 remove() 操作,
     57               *    这行代码对于理解其他代码是有帮助的
     58               */
     59         }
     60 
     61         if (sharedCount(c) == MAX_COUNT)
     62             throw new Error("Maximum lock count exceeded");
     63 
     64         if (compareAndSetState(c, c + SHARED_UNIT)) {
     65             // 这里 CAS 成功,那么就意味着成功获取读锁了
     66             // 下面需要做的是设置 firstReader 或 cachedHoldCounter
     67 
     68             if (sharedCount(c) == 0) {
     69                 // 如果发现 sharedCount(c) 等于 0,就将当前线程设置为 firstReader
     70                 firstReader = current;
     71                 firstReaderHoldCount = 1;
     72             } else if (firstReader == current) {
     73                 firstReaderHoldCount++;
     74             } else {
     75                 // 下面这几行,就是将 cachedHoldCounter 设置为当前线程
     76                 if (rh == null)
     77                     rh = cachedHoldCounter;
     78                 if (rh == null || rh.tid != getThreadId(current))
     79                     rh = readHolds.get();
     80                 else if (rh.count == 0)
     81                     readHolds.set(rh);
     82                 rh.count++;
     83                 cachedHoldCounter = rh;
     84             }
     85             // 返回大于 0 的数,代表获取到了读锁
     86             return 1;
     87         }
     88     }
     89 }

            读完这一段,我觉得有两个问题值得我们思考 :

    问题一 :

           上面的代码还有一句让我们值的思考的是

    rh.count++

           这是在多线程环境下,那这里会不会引起线程安全问题呢?不会,原因rh 来到这里会去ThreadLocal 拿一个值,所以不存在线程问题。

    问题二 :

    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
          cachedHoldCounter = rh = readHolds.get();
    else if (rh.count == 0)
           readHolds.set(rh);
    rh.count++;

           else-if 里的条件什么时候满足呢?可以理解为一个线程先lock 再unlock 又再次lock 的场景。把第一次unlock执行的local执行的remove对象再补充回去的情况。 

          

    读锁释放

    下面我们看看读锁释放的流程:

      1 // ReadLock
      2 public void unlock() {
      3     sync.releaseShared(1);
      4 }
      5 // Sync
      6 public final boolean releaseShared(int arg) {
      7     if (tryReleaseShared(arg)) {
      8         doReleaseShared(); // 这句代码其实唤醒 获取写锁的线程,往下看就知道了
      9         return true;
     10     }
     11     return false;
     12 }
     13 
     14 // Sync
     15 protected final boolean tryReleaseShared(int unused) {
     16     Thread current = Thread.currentThread();
     17     if (firstReader == current) {
     18         if (firstReaderHoldCount == 1)
     19             // 如果等于 1,那么这次解锁后就不再持有锁了,把 firstReader 置为 null,给后来的线程用
     20             // 为什么不顺便设置 firstReaderHoldCount = 0?因为没必要,其他线程使用的时候自己会设值
     21             firstReader = null;
     22         else
     23             firstReaderHoldCount--;
     24     } else {
     25         // 判断 cachedHoldCounter 是否缓存的是当前线程,不是的话要到 ThreadLocal 中取
     26         HoldCounter rh = cachedHoldCounter;
     27         if (rh == null || rh.tid != getThreadId(current))
     28             rh = readHolds.get();
     29 
     30         int count = rh.count;
     31         if (count <= 1) {
     32 
     33             // 这一步将 ThreadLocal remove 掉,防止内存泄漏。因为已经不再持有读锁了
     34             readHolds.remove();
     35 
     36             if (count <= 0)
     37                 // 就是那种,lock() 一次,unlock() 好几次的逗比
     38                 throw unmatchedUnlockException();
     39         }
     40         // count 减 1
     41         --rh.count;
     42     }
     43 
     44     for (;;) {
     45         int c = getState();
     46         // nextc 是 state 高 16 位减 1 后的值
     47         int nextc = c - SHARED_UNIT;
     48         if (compareAndSetState(c, nextc))
     49             // 如果 nextc == 0,那就是 state 全部 32 位都为 0,也就是读锁和写锁都空了
     50             // 此时这里返回 true 的话,其实是帮助唤醒后继节点中的获取写锁的线程
     51             return nextc == 0;
     52     }
     53 }
     54 

            读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。

     

    写锁获取

    1. 写锁是独占锁。
    2. 如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。
      1 // ReadLock
      2 public void unlock() {
      3     sync.releaseShared(1);
      4 }
      5 // Sync
      6 public final boolean releaseShared(int arg) {
      7     if (tryReleaseShared(arg)) {
      8         doReleaseShared(); // 这句代码其实唤醒 获取写锁的线程,往下看就知道了
      9         return true;
     10     }
     11     return false;
     12 }
     13 
     14 // Sync
     15 protected final boolean tryReleaseShared(int unused) {
     16     Thread current = Thread.currentThread();
     17     if (firstReader == current) {
     18         if (firstReaderHoldCount == 1)
     19             // 如果等于 1,那么这次解锁后就不再持有锁了,把 firstReader 置为 null,给后来的线程用
     20             // 为什么不顺便设置 firstReaderHoldCount = 0?因为没必要,其他线程使用的时候自己会设值
     21             firstReader = null;
     22         else
     23             firstReaderHoldCount--;
     24     } else {
     25         // 判断 cachedHoldCounter 是否缓存的是当前线程,不是的话要到 ThreadLocal 中取
     26         HoldCounter rh = cachedHoldCounter;
     27         if (rh == null || rh.tid != getThreadId(current))
     28             rh = readHolds.get();
     29 
     30         int count = rh.count;
     31         if (count <= 1) {
     32 
     33             // 这一步将 ThreadLocal remove 掉,防止内存泄漏。因为已经不再持有读锁了
     34             readHolds.remove();
     35 
     36             if (count <= 0)
     37                 // 就是那种,lock() 一次,unlock() 好几次的逗比
     38                 throw unmatchedUnlockException();
     39         }
     40         // count 减 1
     41         --rh.count;
     42     }
     43 
     44     for (;;) {
     45         int c = getState();
     46         // nextc 是 state 高 16 位减 1 后的值
     47         int nextc = c - SHARED_UNIT;
     48         if (compareAndSetState(c, nextc))
     49             // 如果 nextc == 0,那就是 state 全部 32 位都为 0,也就是读锁和写锁都空了
     50             // 此时这里返回 true 的话,其实是帮助唤醒后继节点中的获取写锁的线程
     51             return nextc == 0;
     52     }
     53 }
     54 

    下面看一眼 writerShouldBlock() 的判定,然后你再回去看一篇写锁获取过程。

      1 static final class NonfairSync extends Sync {
      2     // 如果是非公平模式,那么 lock 的时候就可以直接用 CAS 去抢锁,抢不到再排队
      3     final boolean writerShouldBlock() {
      4         return false; // writers can always barge
      5     }
      6     ...
      7 }
      8 static final class FairSync extends Sync {
      9     final boolean writerShouldBlock() {
     10         // 如果是公平模式,那么如果阻塞队列有线程等待的话,就乖乖去排队
     11         return hasQueuedPredecessors();
     12     }
     13     ...
     14 }
     15 
     

    写锁释放

      1 // WriteLock
      2 public void unlock() {
      3     sync.release(1);
      4 }
      5 
      6 // AQS
      7 public final boolean release(int arg) {
      8     // 1. 释放锁
      9     if (tryRelease(arg)) {
     10         // 2. 如果独占锁释放"完全",唤醒后继节点
     11         Node h = head;
     12         if (h != null && h.waitStatus != 0)
     13             unparkSuccessor(h);
     14         return true;
     15     }
     16     return false;
     17 }
     18 
     19 // Sync 
     20 // 释放锁,是线程安全的,因为写锁是独占锁,具有排他性
     21 // 实现很简单,state 减 1 就是了
     22 protected final boolean tryRelease(int releases) {
     23     if (!isHeldExclusively())
     24         throw new IllegalMonitorStateException();
     25     int nextc = getState() - releases;
     26     boolean free = exclusiveCount(nextc) == 0;
     27     if (free)
     28         setExclusiveOwnerThread(null);
     29     setState(nextc);
     30     // 如果 exclusiveCount(nextc) == 0,也就是说包括重入的,所有的写锁都释放了,
     31     // 那么返回 true,这样会进行唤醒后继节点的操作。
     32     return free;
     33 }

    锁降级

             将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁。回去看下写锁获取的源码:

      1 protected final boolean tryAcquire(int acquires) {
      2 
      3     Thread current = Thread.currentThread();
      4     int c = getState();
      5     int w = exclusiveCount(c);
      6     if (c != 0) {
      7         // 看下这里返回 false 的情况:
      8         //   c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)
      9         //   c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他线程持有写锁
     10         //   也就是说,只要有读锁或写锁被占用,这次就不能获取到写锁
     11         if (w == 0 || current != getExclusiveOwnerThread())
     12             return false;
     13         ...
     14     }
     15     ...
     16 }

          仔细想想,如果线程 a 先获取了读锁,然后获取写锁,(tryAcquire方法返回false)那么线程 a 就到阻塞队列休眠了,自己把自己弄休眠了,而且可能之后就没人去唤醒它了。

    总结

    • rwl 适合读多写少的情景,读-写,写-写,互-斥,而写-读(写读是同一线程)这是被允许的,同时这也被成为锁降级。
    • ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
    • ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
    • ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

    参考资料 :

  • 相关阅读:
    Oracle把表记录恢复到指定时间节点
    解决Idea配置文件不显示中文的问题
    maven过滤配置文件
    文件流字符串互转
    jax-rs下载文件
    Excel导入异常Cannot get a text value from a numeric cell解决及poi导入时注意事项
    oracle获取表字段及表注释的相关操作
    pb SendMessage
    pb datawindow的用法
    charindex函数的用法
  • 原文地址:https://www.cnblogs.com/Benjious/p/10161647.html
Copyright © 2011-2022 走看看