@boothsun
2017-12-03T16:35:13.000000Z
字数 2225
阅读 1528
Java多线程
synchronized和ReentrantLock都是排他锁,这些锁在同一时刻只运行一个线程进行访问,而读写锁在同一时刻可以运行多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发现相比一般的排他锁有了很大的提升。
读写锁适合读多写少的场景。
ReentrantReadWriteLock的特性如下:
ReentrantReadWriteLock实现了ReadWriteLock接口,主要定义了获取读锁和写锁的方法,其内部主要有5个内部类,分别是Sync、NonfairSync、FairSync、ReadLock、WriteLock;Sync、NonfairSync、FairSync是继承自AQS,主要是负责锁状态的维护。ReadLock、WriteLock实现了Lock接口,主要定了获得锁和释放锁等基础操作,其最终的操作还是转换到Sync、NonfairSync、FairSync上。
接下来分析ReentrantReadWriteLock的实现,主要包括:读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。
我们知道,在AQS内部是以单个int类型的原子变量来表示锁状态的,AQS定义了4个抽象方法(tryAcquire(int)、tryRelease(int)、tryAcquireShared(int)、tryReleaseShared(int),前面两个方法用于独占/排他模式,后面两个用于共享模式)留给子类实现,用于自定义同步器的行为以实现特定的功能。
对于ReentrantLock,它是可重入的独占锁。内部的Sync类实现了tryAcquire(int)、tryRelease(int)方法,并用状态的值来表示重入次数,加锁或重入锁时状态加1,释放锁时状态减1,状态值等于0表示锁空闲。
对于CountDownLatch,它是一个关卡,在条件满足前阻塞所有等待线程,条件满足后允许所有线程通过。内部类Sync把状态初始化为大于0的某个值,当状态大于0时所有wait线程阻塞,每调用一次countDown方法就把状态值减1,减为0时运行所有线程通过。利用了AQS的共享模式。
现在,我们知道读写锁同样依赖自定义同步器来实现同步功能,要用AQS来实现 ReentrantReadWriteLock。
一点思考问题:
一点提示:
举个例子:
先来一张读写锁状态划分图:
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
源码分析:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 共享长度
static final int SHARED_SHIFT = 16;
// 由于读锁用高位部分,所以读锁个数加1,其实就是状态值加2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 写锁的可重入最大次数 读锁允许的最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,用状态的低16位有效值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 读锁计数 当前持有读锁的线程数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁的计数,也就是它的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}