前言

上一篇讲解了 AQS 的独占锁部分(参看:ReentrantLock 源码分析以及 AQS (一)),这一篇将介绍 AQS 的共享锁,以及基于共享锁实现读写锁分离的 ReentrantReadWriteLock。(若是遇到之前讲过的方法,将不再赘述)

先思考一下,为什么我们用读写锁分离?

我们知道 ReentrantLock 用的是独占锁,不管线程是读还是写状态,都会阻塞,这无疑会降低并发量。

但是,我们知道多个线程同时去读数据的时候,并不会产生线程安全的问题,因为它们互不干扰。那么为什么不设计一种方案,让所有的读线程可以共享,一起同时读数据呢,只需要阻塞写的线程就可以了。提高并发的同时,也不会产生数据不一致的现象。

同样的,如果有线程在写数据,那么也会阻塞其它读线程(同样阻塞其它写线程),数据写完之后才可以读数据,这样保证读到的数据都是最新的。

因此,我们可以用读、写两把锁,分别控制数据的读和写。实现读读共享、读写互斥,写写互斥。这也是 ReentrantReadWriteLock 读写分离锁的由来。它非常适合用在读多写少的场景。

ReentrantReadWriteLock

它和 ReentrantLock 一样,也是一个可重入的锁,并基于 AQS 共享锁实现了读写分离。其内部结构也大同小异,支持公平锁和非公平锁。我们看下它的构造函数,

  1. public ReentrantReadWriteLock() {
  2. //默认非公平
  3. this(false);
  4. }
  5. public ReentrantReadWriteLock(boolean fair) {
  6. sync = fair ? new FairSync() : new NonfairSync();
  7. readerLock = new ReadLock(this);
  8. writerLock = new WriteLock(this);
  9. }

它定义了两个内部类来表示读锁和写锁,并且都通过内部类 Sync 来实现加锁,释放锁等功能。

  1. public static class ReadLock implements Lock, java.io.Serializable {
  2. private static final long serialVersionUID = -5992448646407690164L;
  3. private final Sync sync;
  4. protected ReadLock(ReentrantReadWriteLock lock) {
  5. sync = lock.sync;
  6. }
  7. ...
  8. }
  9. public static class WriteLock implements Lock, java.io.Serializable {
  10. private static final long serialVersionUID = -4992448646407690164L;
  11. private final Sync sync;
  12. protected WriteLock(ReentrantReadWriteLock lock) {
  13. sync = lock.sync;
  14. }
  15. ...
  16. }
  17. abstract static class Sync extends AbstractQueuedSynchronizer {
  18. }

我们再看下公平锁和非公平锁,其中有两个比较重要的方法,用来判断读锁和写锁是否应该被阻塞,后面加锁的时候会用到(其实,实际情况是否真的应该阻塞,还需要斟酌,后面会说)。

  1. static final class FairSync extends Sync {
  2. private static final long serialVersionUID = -2274990926593161451L;
  3. //公平锁的读和写都需要判断,在它前面是否已经有线程在等待。
  4. //有的话,当前线程就需要阻塞,这也体现了公平性。
  5. final boolean writerShouldBlock() {
  6. return hasQueuedPredecessors();
  7. }
  8. final boolean readerShouldBlock() {
  9. return hasQueuedPredecessors();
  10. }
  11. }
  12. static final class NonfairSync extends Sync {
  13. private static final long serialVersionUID = -8159625535654395037L;
  14. //非公平锁,写的时候不需要阻塞,直接返回false
  15. final boolean writerShouldBlock() {
  16. return false; // writers can always barge
  17. }
  18. final boolean readerShouldBlock() {
  19. //为了避免写线程饥饿,需要判断同步队列中第一个排队的(head.next)是否是独占锁(写线程)
  20. //如果是的话,当前读线程就需要阻塞,这是 AQS 中的方法
  21. return apparentlyFirstQueuedIsExclusive();
  22. }
  23. }
  24. final boolean apparentlyFirstQueuedIsExclusive() {
  25. Node h, s;
  26. return (h = head) != null &&
  27. (s = h.next) != null &&
  28. !s.isShared() &&
  29. s.thread != null;
  30. }

思考:

我们知道 ReentrantLock 的同步状态和重入次数,是直接用 state 值来表示的。那么,现在我需要读和写两把锁,怎么才能用一个 int 类型的值来表示两把锁的状态呢?并且,锁是可重入的,重入的次数怎么记录呢?

别急,下面一个一个说。

怎么用一个 state 值表示读、写两把锁?

state 是一个 32 位的 int 值,读写锁中,把它一分为二,高 16 位用来表示读状态,其值代表读锁的线程数,如图中为 3 个,低 16位表示写状态,其值代表写锁的重入次数(因为是独占锁)。 这样,就可以分别计算读锁和写锁的个数了。其相关的属性和方法定义在 Sync 类中。

  1. static final int SHARED_SHIFT = 16;
  2. //表明读锁每增加一个,state的实际值增加 2^16
  3. static final int SHARED_UNIT = (1 << SHARED_SHIFT);
  4. //写锁的最大重入次数,读锁的最大个数
  5. static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
  6. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  7. //持有读锁的线程个数,参数如的 c 代表 state值
  8. //state 的32位二进制位,无符号右移 16位之后,其实就是高16位的值
  9. static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
  10. //写锁数量,即写锁的重入次数
  11. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读锁的个数计算比较简单,直接无符号右移 16 位即可。我们看下写锁的重入次数是怎么计算的。先看下 EXCLUSIVE_MASK 这个值,是 (1 << 16) - 1,我们用二进制表示计算过程为:

  1. // 1的二进制
  2. 0000 0000 0000 0000 0000 0000 0000 0001
  3. // 1左移 16位
  4. 0000 0000 0000 0001 0000 0000 0000 0000
  5. //再减 1
  6. 0000 0000 0000 0000 1111 1111 1111 1111
  7. //任何一个 32位二进制数 c,和以上值做 “与” 运算都为它本身 c 的低 16 位值
  8. //这个不用解释了吧,这个不会的话,需要好好补充一下基础知识了。。。

锁的重入次数是怎么计算的?

写锁比较简单,直接用计算出来的低16位值就可以代表写锁的重入次数。

读锁,就比较复杂了,因为高16位只能表示持有共享锁的线程个数,实在是分身乏术啊。所以,在 Sync 内部,维护了一个类,用来表示每个线程重入的次数,

  1. static final class HoldCounter {
  2. int count = 0;
  3. // Use id, not reference, to avoid garbage retention
  4. final long tid = getThreadId(Thread.currentThread());
  5. }

这里边定义了一个计数器来表示重入次数,tid 来表示当前的线程 id 。但是,这样还不够,我们需要把 HoldCounter 和 线程绑定,这样才可以区分出来每个线程分别持有的锁个数(重入次数),这就需要用到 ThreadLocal 了。

  1. static final class ThreadLocalHoldCounter
  2. extends ThreadLocal<HoldCounter> {
  3. //重写此方法,可以在 ThreadLocal 没有当前线程计数的情况下,
  4. //直接使用 的 get 方法,初始化一个,而不必 new 一个对象
  5. public HoldCounter initialValue() {
  6. return new HoldCounter();
  7. }
  8. }

除此之外,Sync 中还定义了一些其他和读锁相关的属性,

  1. //保存了当前线程重入的读锁次数,当重入次数减到 0 时移除
  2. //移除应该是为了性能着想,因为可以随时通过 get 方法初始化 HoldCounter
  3. private transient ThreadLocalHoldCounter readHolds;
  4. //保存了最近一个获取读锁成功的线程计数,这个变量的目的是:
  5. //如果最后一个获取到读锁的线程重复获取读锁,那么就可以直接拿来用,而不用更新。
  6. //相当于缓存,提高效率
  7. private transient HoldCounter cachedHoldCounter;
  8. //第一个获取读锁的线程
  9. private transient Thread firstReader = null;
  10. //第一个获取读锁的线程计数
  11. private transient int firstReaderHoldCount;
  12. //这两个参数,是为了效率问题,当只有一个线程获得读锁时,就避免了查找 readHolds

基本知识讲完啦,那么接下来就是锁的获取和释放了。先说下写锁吧,因为有上一篇独占锁的基础了,理解起来比较容易。

写锁的获取

写锁的获取从 lock 方法开始,

  1. //ReentrantReadWriteLock.WriteLock.lock
  2. public void lock() {
  3. sync.acquire(1);
  4. }
  5. //AbstractQueuedSynchronizer.acquire
  6. public final void acquire(int arg) {
  7. if (!tryAcquire(arg) &&
  8. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  9. selfInterrupt();
  10. }
  11. //公平锁和非公平锁调用的是同一个方法,在 Sync 类中定义
  12. //ReentrantReadWriteLock.Sync.tryAcquire
  13. protected final boolean tryAcquire(int acquires) {
  14. Thread current = Thread.currentThread();
  15. //获取同步状态 state
  16. int c = getState();
  17. //写锁状态
  18. int w = exclusiveCount(c);
  19. //如果同步状态不为 0,说明有线程获得了读锁或写锁
  20. if (c != 0) {
  21. //如果同步状态不为 0 ,并且写锁状态为 0,说明了读锁被占用,因读写锁互斥,故返回 false
  22. //若写锁状态不为 0,并且不是当前线程获得了写锁,则不能重入,返回 false
  23. if (w == 0 || current != getExclusiveOwnerThread())
  24. return false;
  25. //如果超过了最大写锁数量,则抛出异常
  26. if (w + exclusiveCount(acquires) > MAX_COUNT)
  27. throw new Error("Maximum lock count exceeded");
  28. //若走到这一步,说明当前线程重入了,则计算重入次数,返回true
  29. setState(c + acquires);
  30. return true;
  31. }
  32. //到这说明 c 为 0,读锁和写锁都没有被占用
  33. //如果写锁应该被阻塞或者 CAS 获取写锁失败,则返回false
  34. if (writerShouldBlock() ||
  35. !compareAndSetState(c, c + acquires))
  36. return false;
  37. //把当前线程设为独占锁的所有者
  38. setExclusiveOwnerThread(current);
  39. return true;
  40. }

写锁的释放

同理,写锁的释放从 unlock 方法开始,

  1. public void unlock() {
  2. sync.release(1);
  3. }
  4. public final boolean release(int arg) {
  5. if (tryRelease(arg)) {
  6. Node h = head;
  7. if (h != null && h.waitStatus != 0)
  8. unparkSuccessor(h);
  9. return true;
  10. }
  11. return false;
  12. }
  13. protected final boolean tryRelease(int releases) {
  14. //若独占锁的持有者不是当前线程,则抛出异常
  15. if (!isHeldExclusively())
  16. throw new IllegalMonitorStateException();
  17. //每次释放,state 减 1
  18. int nextc = getState() - releases;
  19. boolean free = exclusiveCount(nextc) == 0;
  20. if (free)
  21. setExclusiveOwnerThread(null);
  22. setState(nextc);
  23. return free;
  24. }

可以看到,写锁的获取和释放和 ReentrantLock 的基本思想是差不多的。下面,着重讲解读锁的获取和释放,相对比较复杂。

读锁的获取

tryAcquireShared

从 ReadLock.lock 方法开始,

  1. public void lock() {
  2. //调用 AQS 的方法
  3. sync.acquireShared(1);
  4. }
  5. public final void acquireShared(int arg) {
  6. //如果 tryAcquireShared 方法返回小于 0,说明获取读锁失败
  7. if (tryAcquireShared(arg) < 0)
  8. //以共享模式加入同步队列,再自旋抢锁
  9. doAcquireShared(arg);
  10. }
  11. protected final int tryAcquireShared(int unused) {
  12. Thread current = Thread.currentThread();
  13. int c = getState();
  14. //如果有线程获取到了写锁,并且不是当前线程,则返回 -1 。
  15. //这是因为,如果线程先获得了写锁,是可以重入再次获取读锁的,此为锁降级。
  16. //否则不可重入。
  17. if (exclusiveCount(c) != 0 &&
  18. getExclusiveOwnerThread() != current)
  19. return -1;
  20. //读锁数量
  21. int r = sharedCount(c);
  22. //如果同时满足以下三个条件(读线程不应该被阻塞,读锁数量小于最大数量限制,CAS成功),
  23. //则说明获取读锁成功,返回 1。然后再设置相关属性的值。
  24. if (!readerShouldBlock() &&
  25. r < MAX_COUNT &&
  26. compareAndSetState(c, c + SHARED_UNIT)) {
  27. //如果读锁状态为 0,说明还没有其他线程获取到读锁
  28. if (r == 0) {
  29. //就把当前线程设置为第一个获取到读锁的线程
  30. firstReader = current;
  31. //第一个读线程计数设置为 1
  32. firstReaderHoldCount = 1;
  33. } else if (firstReader == current) {
  34. //如果当前线程是第一个获取读锁的线程,则重入,计数加 1
  35. firstReaderHoldCount++;
  36. } else { //读锁状态不为 0,并且当前线程不是 firstReader
  37. //最近一个成功获取到读锁的线程计数器
  38. HoldCounter rh = cachedHoldCounter;
  39. //如果计数器为空,或者计数器的 tid不是当前线程 id,说明有两种情况
  40. //1.rh 还未被任何线程设置,此时只有 firstReader 一个线程获取到了读锁。
  41. //2.rh 已经被设置了,并且不是当前线程,说明在当前线程之前,除了 firstReader,
  42. //还有其他线程获取到了读锁,那么当前线程就是第三个获取到读锁的(至少)。
  43. if (rh == null || rh.tid != getThreadId(current))
  44. //不管哪种情况,都需要创建并初始化当前线程的计数器,并赋值给 cachedHoldCounter
  45. //因为,当前线程是此时最后一个获取到读锁的线程,需要缓存下来
  46. cachedHoldCounter = rh = readHolds.get();
  47. //如果当前线程是最近一个获取到读锁的线程,并且计数为0,
  48. else if (rh.count == 0)
  49. //就把 rh 线程持有锁的次数信息,放入到本地线程 readHolds
  50. readHolds.set(rh);
  51. //最后把计数加 1
  52. rh.count++;
  53. }
  54. return 1;
  55. }
  56. //若以上三个条件任意一个不满足,则调用此方法,再次全力尝试获取锁
  57. return fullTryAcquireShared(current);
  58. }

fullTryAcquireShared 这个方法和 tryAcquireShared 方法非常相似,只是多了一个自旋的过程,直到返回一个确定值(-1或1),才结束。

  1. final int fullTryAcquireShared(Thread current) {
  2. HoldCounter rh = null;
  3. //自旋,直到返回一个确定值(1或 -1)
  4. for (;;) {
  5. int c = getState();
  6. //如果写锁状态不为0,说明有线程获取到了写锁
  7. if (exclusiveCount(c) != 0) {
  8. //获取到写锁的线程不是当前线程,则返回 -1
  9. if (getExclusiveOwnerThread() != current)
  10. return -1;
  11. //这里省略了else,到这里说明了当前线程获取到了写锁,因此需要做锁降级处理,
  12. //把写锁降级为读锁。因为如果不这样做的话,线程就会阻塞到这,会导致死锁。
  13. //然后跳转到 ①处继续执行
  14. //===========//
  15. } else if (readerShouldBlock()) { //写锁空闲,并且读锁应该阻塞,说明 head.next正在等待获取写锁
  16. //尽管读锁应该阻塞,但是此处也不应该立即阻塞,因为有可能存在读锁重入,需要再确认一下。
  17. if (firstReader == current) {//当前线程是第一个读锁,可重入
  18. // 将跳转到 ①处
  19. } else {
  20. if (rh == null) { //第一次循环进来时肯定为 null
  21. rh = cachedHoldCounter; //取到缓存中最后一次获取到读锁的计数器
  22. if (rh == null || rh.tid != getThreadId(current)) {
  23. rh = readHolds.get();
  24. //计数为 0,说明当前线程没有获取到过读锁
  25. if (rh.count == 0)
  26. //为了性能考虑,如果计数为 0,需要把它移除掉
  27. readHolds.remove();
  28. }
  29. }
  30. //走到这,说明当前线程不是 firstReader,也没有获取到过读锁,不符合重入条件,
  31. //那么就确定需要阻塞,只能去排队了,返回 -1 。
  32. if (rh.count == 0)
  33. return -1;
  34. }
  35. }
  36. // ①处
  37. //如果读锁数量达到了 MAX_COUNT,则抛出异常
  38. if (sharedCount(c) == MAX_COUNT)
  39. throw new Error("Maximum lock count exceeded");
  40. //CAS获取读锁,和 tryAcquireShared 的处理逻辑一样,不再赘述
  41. if (compareAndSetState(c, c + SHARED_UNIT)) {
  42. if (sharedCount(c) == 0) {
  43. firstReader = current;
  44. firstReaderHoldCount = 1;
  45. } else if (firstReader == current) {
  46. firstReaderHoldCount++;
  47. } else {
  48. if (rh == null)
  49. rh = cachedHoldCounter;
  50. if (rh == null || rh.tid != getThreadId(current))
  51. rh = readHolds.get();
  52. else if (rh.count == 0)
  53. readHolds.set(rh);
  54. rh.count++;
  55. cachedHoldCounter = rh; // cache for release
  56. }
  57. return 1;
  58. }
  59. }
  60. }

doAcquireShared

如果 tryAcquireShared 最终还是失败了,那么就执行 doAcquireShared 方法。

  1. private void doAcquireShared(int arg) {
  2. //以共享模式加入同步队列
  3. final Node node = addWaiter(Node.SHARED);
  4. boolean failed = true;
  5. try {
  6. boolean interrupted = false;
  7. for (;;) {
  8. final Node p = node.predecessor();
  9. if (p == head) {
  10. //如果当前节点的前驱节点是头结点,再次尝试获取读锁
  11. int r = tryAcquireShared(arg);
  12. if (r >= 0) {
  13. //把当前节点设置为头结点,并把共享状态传播下去
  14. setHeadAndPropagate(node, r);
  15. p.next = null; // help GC
  16. if (interrupted)
  17. selfInterrupt();
  18. failed = false;
  19. return;
  20. }
  21. }
  22. //获取读锁失败,判断是否可挂起当前线程
  23. if (shouldParkAfterFailedAcquire(p, node) &&
  24. parkAndCheckInterrupt())
  25. interrupted = true;
  26. }
  27. } finally {
  28. if (failed)
  29. cancelAcquire(node);
  30. }
  31. }

setHeadAndPropagate

  1. private void setHeadAndPropagate(Node node, int propagate) {
  2. //旧的头结点
  3. Node h = head;
  4. //把当前 node 设置为新的头结点
  5. setHead(node);
  6. //propagate 是 tryAcquireShared 方法的返回值
  7. //若大于0,或者旧的头结点为空,或者头结点的 ws 小于0
  8. //又或者新的头结点为空,或者新头结点的 ws 小于0,则获取后继节点
  9. if (propagate > 0 || h == null || h.waitStatus < 0 ||
  10. (h = head) == null || h.waitStatus < 0) {
  11. Node s = node.next;
  12. //没有后继节点或者后继节点是共享节点,就执行唤醒
  13. if (s == null || s.isShared())
  14. //释放掉资源,并唤醒后继节点,稍后讲解
  15. doReleaseShared();
  16. }
  17. }

读锁的释放

tryReleaseShared

从 ReadLock.unlock方法开始,

  1. public void unlock() {
  2. sync.releaseShared(1);
  3. }
  4. public final boolean releaseShared(int arg) {
  5. if (tryReleaseShared(arg)) {
  6. doReleaseShared();
  7. return true;
  8. }
  9. return false;
  10. }
  11. protected final boolean tryReleaseShared(int unused) {
  12. Thread current = Thread.currentThread();
  13. //当前线程为第一个读线程
  14. if (firstReader == current) {
  15. //若 firstReader 的计数为1,则把它置为 null
  16. if (firstReaderHoldCount == 1)
  17. firstReader = null;
  18. else
  19. //否则,计数减 1,说明重入次数减 1
  20. firstReaderHoldCount--;
  21. } else {
  22. HoldCounter rh = cachedHoldCounter;
  23. if (rh == null || rh.tid != getThreadId(current))
  24. rh = readHolds.get();
  25. int count = rh.count;
  26. if (count <= 1) {
  27. //如果当前线程的计数小于等于 1,则移除
  28. readHolds.remove();
  29. if (count <= 0)
  30. //若计数小于等于 0,则抛出异常
  31. throw unmatchedUnlockException();
  32. }
  33. //计数减 1
  34. --rh.count;
  35. }
  36. for (;;) {
  37. int c = getState();
  38. //读锁状态减 1,其实就是state值减 65536
  39. //因为高16位的读锁实际值,在state中的表现就是相差 65536
  40. int nextc = c - SHARED_UNIT;
  41. // CAS 设置 state 最新状态
  42. if (compareAndSetState(c, nextc))
  43. //如果读锁状态减为 0,就返回true
  44. //释放读锁对其它读线程没有任何影响,
  45. //但是如果读、写锁都空闲,就可以允许等待的写线程继续执行
  46. return nextc == 0;
  47. }
  48. }

doReleaseShared

如果 tryReleaseShared 方法返回 true,说明读锁释放成功,需要唤醒后继节点,

  1. private void doReleaseShared() {
  2. for (;;) {
  3. //头结点
  4. Node h = head;
  5. //说明队列中至少有两个节点
  6. if (h != null && h != tail) {
  7. int ws = h.waitStatus;
  8. if (ws == Node.SIGNAL) {
  9. //如果头结点的 ws 为 -1 ,则 CAS 把它设置为 0,因为唤醒后继节点后,
  10. //它就不需要做什么了。失败继续自旋尝试
  11. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  12. continue; // loop to recheck cases
  13. // CAS 成功,则唤醒后继节点
  14. unparkSuccessor(h);
  15. }
  16. //如果 ws 为 0,则把它设置为 -3 ,表明共享状态可向后传播,失败则继续自旋尝试
  17. //后来我一直在想,为什么需要设置一个 PROPAGATE 这样的状态呢,但是还没头绪
  18. //可以看下这篇文章分析,或许有一定的参考价值:
  19. //https://www.cnblogs.com/micrari/p/6937995.html
  20. //只能说 Doug Lea 大神的逻辑真是太缜密了,等我以后想明白了,再补充吧。
  21. //可以暂时先理解为,这就是一个无条件传播的标志
  22. else if (ws == 0 &&
  23. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  24. continue; // loop on failed CAS
  25. }
  26. //如果此刻 h 等于头结点,说明头结点未改变,则跳出整个循环
  27. //否则,说明头结点被其他线程修改过了,则继续下一次的循环判断
  28. if (h == head) // loop if head changed
  29. break;
  30. }
  31. }

结语

关于独占锁,比较简单。而读锁,涉及到了很多临界点和瞬时状态。其实细想,并不像表面上看起来那么简单,理解的会比较浅显,毕竟 Doug Lea 大神的思想不是常人能揣摩透的。

本篇只是我的一些个人理解,如有讲解不到位的地方,欢迎拍砖。

其实,还有很多细节问题,本文并没有展开。例如, setHeadAndPropagate 方法为什么判断两次新旧节点的 ws 状态,意义何为。 doReleaseShared 方法最后为什么需要设计 h == head 这样的判断,有什么含义。包括为什么要设计 PROPAGATE 状态,没有这个状态又如何。

看来路阻且长啊。。。以后再来补坑吧,这篇只能叫浅析了。 ̄□ ̄||

如果本文对你有用,欢迎点赞,评论,转发。

学习是枯燥的,也是有趣的。我是「烟雨星空」,欢迎关注,可第一时间接收文章推送。

ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)的更多相关文章

  1. 【Java并发编程】16、ReentrantReadWriteLock源码分析

    一.前言 在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁 ...

  2. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  3. ReentrantLock 源码分析以及 AQS (一)

    前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...

  4. ReentrantReadWriteLock 源码分析

    ReentrantReadWriteLock  源码分析: 1:数据结构: 成员变量: private final ReentrantReadWriteLock.ReadLock readerLock ...

  5. 手机自动化测试:appium源码分析之bootstrap十二

    手机自动化测试:appium源码分析之bootstrap十二   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣 ...

  6. HDFS源码分析之UnderReplicatedBlocks(二)

    UnderReplicatedBlocks还提供了一个数据块迭代器BlockIterator,用于遍历其中的数据块.它是UnderReplicatedBlocks的内部类,有三个成员变量,如下: // ...

  7. ReentrantReadWriteLock源码分析(一)

    此处源码分析,主要是基于读锁,非公平机制,JDK1.8. 问题: 1.ReentrantReadWriteLock是如何创建读锁与写锁? 2.读锁与写锁的区别是什么? 3.锁的重入次数与获取锁的线程数 ...

  8. Java显式锁学习总结之五:ReentrantReadWriteLock源码分析

    概述 我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放.共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和 ...

  9. ReentrantReadWriteLock源码分析笔记

    ReentrantReadWriteLock包含两把锁,一是读锁ReadLock, 此乃共享锁, 一是写锁WriteLock, 此乃排它锁. 这两把锁都是基于AQS来实现的. 下面通过源码来看看Ree ...

随机推荐

  1. sqlite基础API

    /* 打开/创建数据库文件 * 如果数据库文件不存在就创建数据库文件. * 数据库操作句柄保存在第二个参数中. * 第一个参数:文件路径及其文件名 * 第二个参数:sqlite3操作句柄 * 返回值: ...

  2. Linux下重要文件

    1:/etc/sysconfig/network-scripts/ifcfg-ens130 2:   /etc/resolv.conf   DNS配置文件 3:/etc/hosts 4:/etc/sy ...

  3. spring security梳理

    核心服务:AuthenticationManager,UserDetailsService和AccessDecisionManager The AuthenticationManager, Provi ...

  4. telnet不是内部或外部命令的问题解决

    在windows DOS 命令窗口中输入telnet命令,出现如下提示信息:     可能是没有安装telnet客户端的原因,又得开始捣鼓了  www.2cto.com     依次点击"开 ...

  5. Docker企业级镜像仓库harbor(vmware 中国团队)

    第一步:安装docker和docker-compose 第二步:下载harbor-offline-installer-v1.3.0.tgz 第三步:上传到/opt,并解压 第四步:修改harbor.c ...

  6. 吴裕雄--天生自然python编程:pycharm常用快捷键问题

    最近在使用pycharm的时候发现不能正常使用ctrl+c/v进行复制粘贴,也无法使用tab键对大段代码进行整体缩进.后来发现是因为安装了vim插件的问题,在setting里找到vim插件,取消勾选即 ...

  7. 吴裕雄--天生自然 R语言开发学习:重抽样与自助法(续一)

    #-------------------------------------------------------------------------# # R in Action (2nd ed): ...

  8. react 踩坑记

    yarn  node-sass 安装失败 yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass yarn i ...

  9. 对RLC重排序窗口大小的一点讨论

    在LTE协议栈的PDCP层和RLC层,都有一个重排序窗口(reordering window),主要用来保证数据的可靠传输,PDCP层的重排序窗口主要用于handover时保证数据的可靠传输,这里暂且 ...

  10. OpenStack入门

    云计算优势 降低成本,安全稳定,易扩展. 云计算三种服务模式 IaaS:基础设施即服务 IaaS(Infrastructure-as-a- Service):基础设施即服务.消费者通过Internet ...