深入浅出Java并发包—读写锁ReentrantReadWriteLock原理分析(二)
2016-04-17 10:48
501 查看
下面我们再来看下读锁的实现:
读锁的实现稍稍有点复杂,我们来慢慢分析一下:
1、如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程,那么就返回失败。因为允许写入线程获取锁的同时获取读取锁。
2、如果读线程请求锁数量达到了65535(包括重入锁),那么就抛出一个错误Error
3、如果读线程不用等待(实际上是是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功(这里有一个HoldCounter对象,我们后面再来分析)
4、步骤3失败(CAS操作失败),那么就需要循环不断尝试去修改状态直到成功或者锁被写入线程占有。实际上是步骤3的不断尝试 直到CAS计数成功或者被写入线程占有锁。
同样值得注意的是,这里计算读线程的数目的算法!
直接将c向右无符号移位16位,低位数据被完全舍弃,剩余的高位数字被转移到低位,正好显示的就是高16位的读锁线程的数目。当然执行CAS加法的时候,需要注意要加的是1<<16(需要直接加65536,直接写入高16位)
下面我们再来看下对应锁资源的释放过程。同样我们先来看写锁的过程:
这段代码相对比较简单,首先看当前线程是不是写锁的拥有者,毕竟其他线程是没有权限释放别的线程的所资源的,如果不是同一个线程则抛出异常。然后检测释放后写线程计数器是否为0,如果为0说明写锁已经没有线程使用,释放写锁资源,并回写锁占用情况为0,否则就是说当前是重入锁的一次释放,所以不能将独占锁线程清空。然后将剩余线程状态数写回AQS。
写锁的释放相对简单,我们再来看一下读锁的释放:
同样先不理会HoldCounter的存在,关键的在于for循环里面,其实就是一个不断尝试的CAS操作,直到修改状态成功。前面说过state的高16位描述的共享锁(读取锁)的数量,所以每次都需要减去2^16,这样就相当于读取锁数量减1。我们也看到SHARED_UNIT=1<<16=2^16。
看到这里,可能大家最疑惑的的就是HoldCounter对象了,这到底是什么呢?我们来看下他的源代码:
首先我们可以看到这个实例只有在获取共享锁(读取锁)的时候加1,也只有在释放共享锁的时候减1有作用,并且在释放锁的时候如果tryDecrement() <= 0则抛出了一个IllegalMonitorStateException异常。而我们知道IllegalMonitorStateException通常描述的是一个线程操作一个不属于自己的监视器对象的引发的异常。也就是说这里的意思是一个线程释放了一个不属于自己或者不存在的共享锁。
前面的我们也说起过,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一次计数器操作,一次获取共享锁相当于计数器加1,释放一个共享锁就相当于计数器减1。显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有锁的线程数,包括重入数)错误。
明白了HoldCounter的作用后我们就可以猜到它的作用其实就是当前线程持有共享锁(读取锁)的数量,包括重入的数量。那么这个数量就必须和线程绑定在一起。
在Java里面将一个对象和线程绑定在一起,就只有ThreadLocal才能实现了。所以毫无疑问HoldCounter就应该是绑定到线程上的一个计数器。而ThreadLocalHoldCounter就是线程绑定的ThreadLocal。
可以看到这里使用ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
这里注意,很多人都会觉得get方法会获取当前线程对应的计数器,那是什么时候set进去的呢?我们好像没有看到哪里有set的行为。我们再来看下ThredLocal的API。
ThredLocal有一个initialValue的方法,如果没有调用则默认使用这个方法提供的数据,而ThreadLocalHoldCounter重写了这个方法。
当该线程对应的数据不存在时则重建一个新的计数器,否则可以直接拿到之前线程设置进去的计数器。
问题又来了,前面我们提到共享锁其实就是一个计数器,那为啥这里要设计一个计数器和线程绑定,为啥不直接公用一个计数器呢?
前面我们也说了,readlock和writeLock是不是两把锁呢?显然他们公用了一个Sync,公用了一个CLH队列,假设还采用一个计数器的话,那么一旦读线程获取了锁,后续再来读线程,发现是共享模式就可以一直持有,不断的释放不断的持有,那么写入线程就处于死等待状态了!显然为了解决这个问题,就是通过线程来区分(要区分是线程重入的还是另外线程发起的)。我们再来看下读线程的readerShouldBlock方法
公平锁显然会按照进入队列的顺序执行,而非公平锁会判断下一个节点是否是写锁线程,如果是写锁线程则直接阻塞当前读线程。而非公平锁判断阻塞的方法则是直接返回false。
很显然,在非公平锁模式下,写线程是优先于读线程的。
有了前面AQS的基础,理解这些都不是特别难了,真正的东西还是在AQS里面。可参考《深入浅出Java并发包—锁(Lock)VS同步(synchronized)》、《锁机制一、二、三》。
注1:锁升级降级的实现示例
注2:移位操作
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (!readerShouldBlock(current)&&compareAndSetState(c,c+SHARED_UNIT)) { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) cachedHoldCounter = rh = readHolds.get(); rh.count++; return 1; } return fullTryAcquireShared(current); } final int fullTryAcquireShared(Thread current) { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) rh = readHolds.get(); for (;;) { int c = getState(); int w = exclusiveCount(c); if ((w != 0 && getExclusiveOwnerThread() != current) || ((rh.count | w) == 0 && readerShouldBlock(current))) return -1; if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { cachedHoldCounter = rh; // cache for release rh.count++; return 1; } } } |
1、如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程,那么就返回失败。因为允许写入线程获取锁的同时获取读取锁。
2、如果读线程请求锁数量达到了65535(包括重入锁),那么就抛出一个错误Error
3、如果读线程不用等待(实际上是是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功(这里有一个HoldCounter对象,我们后面再来分析)
4、步骤3失败(CAS操作失败),那么就需要循环不断尝试去修改状态直到成功或者锁被写入线程占有。实际上是步骤3的不断尝试 直到CAS计数成功或者被写入线程占有锁。
同样值得注意的是,这里计算读线程的数目的算法!
static int sharedCount(int c) { return c >>> SHARED_SHIFT; } |
static final int SHARED_UNIT = (1 << SHARED_SHIFT); compareAndSetState(c, c + SHARED_UNIT) |
protected final boolean tryRelease(int releases) { int nextc = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); if (exclusiveCount(nextc) == 0) { setExclusiveOwnerThread(null); setState(nextc); return true; } else { setState(nextc); return false; } } |
写锁的释放相对简单,我们再来看一下读锁的释放:
protected final boolean tryReleaseShared(int unused) { HoldCounter rh = cachedHoldCounter; Thread current = Thread.currentThread(); if (rh == null || rh.tid != current.getId()) rh = readHolds.get(); if (rh.tryDecrement() <= 0) throw new IllegalMonitorStateException(); for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) return nextc == 0; } } |
看到这里,可能大家最疑惑的的就是HoldCounter对象了,这到底是什么呢?我们来看下他的源代码:
static final class HoldCounter { int count; // Use id, not reference, to avoid garbage retention final long tid = Thread.currentThread().getId(); /** Decrement if positive; return previous value */ int tryDecrement() { int c = count; if (c > 0) count = c - 1; return c; } } static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } } |
前面的我们也说起过,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一次计数器操作,一次获取共享锁相当于计数器加1,释放一个共享锁就相当于计数器减1。显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有锁的线程数,包括重入数)错误。
明白了HoldCounter的作用后我们就可以猜到它的作用其实就是当前线程持有共享锁(读取锁)的数量,包括重入的数量。那么这个数量就必须和线程绑定在一起。
在Java里面将一个对象和线程绑定在一起,就只有ThreadLocal才能实现了。所以毫无疑问HoldCounter就应该是绑定到线程上的一个计数器。而ThreadLocalHoldCounter就是线程绑定的ThreadLocal。
可以看到这里使用ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
这里注意,很多人都会觉得get方法会获取当前线程对应的计数器,那是什么时候set进去的呢?我们好像没有看到哪里有set的行为。我们再来看下ThredLocal的API。
构造方法摘要 |
---|
ThreadLocal() 创建一个线程本地变量。 |
方法摘要 | |
---|---|
T | get() 返回此线程局部变量的当前线程副本中的值。 |
protected T | initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void | remove() 移除此线程局部变量当前线程的值。 |
void | set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。 |
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } } |
问题又来了,前面我们提到共享锁其实就是一个计数器,那为啥这里要设计一个计数器和线程绑定,为啥不直接公用一个计数器呢?
前面我们也说了,readlock和writeLock是不是两把锁呢?显然他们公用了一个Sync,公用了一个CLH队列,假设还采用一个计数器的话,那么一旦读线程获取了锁,后续再来读线程,发现是共享模式就可以一直持有,不断的释放不断的持有,那么写入线程就处于死等待状态了!显然为了解决这个问题,就是通过线程来区分(要区分是线程重入的还是另外线程发起的)。我们再来看下读线程的readerShouldBlock方法
公平锁: final boolean readerShouldBlock(Thread current) { // only proceed if queue is empty or current thread at head return !isFirst(current); } 非公平锁: final boolean readerShouldBlock(Thread current) { return apparentlyFirstQueuedIsExclusive(); } final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return ((h = head) != null && (s = h.next) != null && s.nextWaiter != Node.SHARED); } |
final boolean writerShouldBlock(Thread current) { return false; // writers can always barge } |
有了前面AQS的基础,理解这些都不是特别难了,真正的东西还是在AQS里面。可参考《深入浅出Java并发包—锁(Lock)VS同步(synchronized)》、《锁机制一、二、三》。
注1:锁升级降级的实现示例
class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } } |
1、左移位:<<,有符号的移位操作,左移操作时将运算数的二进制码整体左移指定位数,左移之后的空位用0补充 2、右移位:>>,有符号的移位操作,右移操作是将运算数的二进制码整体右移指定位数,右移之后的空位用符号位补充,如果是正数用0补充,负数用1补充。 3、无符号右移位:>>>,无符号的移位操作,右移操作是将运算数的二进制码整体右移指定位数,右移之后的空位用符号位补充,无论正负,全部补零。 注意:移位操作的数据类型可以是byte, char, short, int, long型,但是对byte, char, short进行操作时会先把它们变成一个int型,最后得到一个int型的结果,对long型操作时得到一个long型结果,不可以对boolean型进行操作。 简单计算:x>>y=x/2^y,取整数;x<<y=x*2^y,取整数 精度损失:运算过程中可能造成精度损失,如下示例
127变成int型,左移2位得到 508,然后把508赋给byte型变量a时只是简单地"折断"(truncate)得到数-4。编译时编译器不会提示你可能损失精度(实际上在本例中确实是损失精度了),但是如果你把a <<= 2改成 a = a << 2;编译器就会提示可能损失精度了。 |
相关文章推荐
- 安装eclipse的JRebel6.0.3的插件
- java基础(3)--java.lang.ClassLoader类的用法
- java冒泡排序
- 解密JVM GC
- Spring MVC整体处理流程
- 深入浅出Java并发包—锁机制(一)
- java中的缓存
- 【Java学习-J.160411.0.8】笔记7-Java第一个hello world学习
- struts2中拦截器实现的三种方式
- java native关键字
- 20145236 《Java程序设计》实验二实验报告
- 201301 JAVA题目0-1级
- 关于JDK和JRE
- java虚拟机Class类文件的结构
- struts2中配置拦截器、拦截器栈和默认拦截器
- eclipse设置全局编码为UTF-8的方法
- 20160416--javaweb之国际化
- java之判断字符char为空
- java读取指定package下的所有class
- JAVA中的继承