官方文档内的一些介绍:
1、成功获取读锁的线程将会看到之前已经释放完毕的写锁做的所有更新操作
2、ReentrantReadWriteLock可以确保同一时刻只有一个线程能够修改共享数据,与此相对的,大多数情况下可以有多个线程同时读取同一个数据
3、它支持和ReentrantLock一样的机制,也就说它支持公平和非公平机制
公平:当前锁被释放后,在等待队列中等待时间最长的写线程会被分配到写锁;或者在等待队列中有一组读线程等待时间比所有写线程的等待时间更长,这一组读锁会被分配到读锁
4、写锁可以获取读锁,但读锁尝试获取写锁的操作永远不会成功
锁降级:
重入性允许写锁降级成读锁,顺序为:获取写锁–>获取读锁–>释放写锁,而读锁向写锁的升级是不可能的
5、写锁支持Condition队列,而读锁不支持,并会抛出UnsupportedOperationException异常
官方使用方法:
一、
class CachedData {
Object data;
//此变量为缓存值,使用volatile修饰,确保了共享数据对多线程的可见性
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
rwl.readLock().unlock();
rwl.writeLock().lock();
//重新核查,确保数据没有因为并发被其它写线程修改
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 在释放写锁之前获取读锁来达到降级的目的
rwl.readLock().lock();
rwl.writeLock().unlock();
}
use(data);
rwl.readLock().unlock();
}
}
上述代码执行流程:
1、获取读锁尝试读取数据。若缓存失效,释放读锁 ,并加写锁(因为再次期间因为并发可能有其他写线程获的了锁修改果缓存数据,所以再次进行判断)二次判断如果缓存数据仍然失效(即没有被其它写线程修改),则开始写入数据。数据写入完毕,在释放写锁之前先获取读锁(防止期间写线程获取锁修改数据,导致没有返回最新缓存),然后释放写锁(因为此时已经成功获取读锁所以写线程在此时不可能获取锁,因此缓存数据此时不会被修改),接着写线程开始读取缓存,完毕后释放
这里的锁降级其实是一种重入机制,只有持有当前写锁的线程才能在写锁没有释放的同时去加读锁,其它线程的加锁操作是不会成功的。
二、
class RWDictionary {
private final Map m = new TreeMap();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}}
ReentrantReadWriteLock的具体实现:
公用变量:
/** 读锁,由内部类ReadLock来提供 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁,由内部类WriteLock来提供 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 内部类Sync的对象,AQS的子类,提供所有的同步机制 */
final Sync sync;
构造方法:
/**默认非公平模式*/
public ReentrantReadWriteLock() {
this(false);
}
/**自选模式*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
两个调用方法:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
接下来看ReentrantReadWriteLock的几个重要内部类:
内部类ReadLock :
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
内部类WriteLock:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
}
可以看到,和ReentrantLock一样,ReentrantReadWriteLock的读和写锁的lock、unlock方法都使用了AQS中的acquire、release、acquireShared、releaseShared方法,因为AQS中并没有这些方法相应的实现,所以ReentrantReadWriteLock通过一个内部类Sync来继承AQS类,由此类来实现相应的方法。
这正好印证了AQS官方文档中的解释:
“AQS子类们应该被定义为非公开的内部辅助类。并且被用来实现它们外部类的其它内部类(enclosing)的同步属性”
因此ReentrantReadWriteLock的重点是内部类Sync,看一下内部类Sync的一些属性和方法:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
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; }
以上六个都是此类的静态属性
state: AQS中的原子值state,AQS的子类Sync通过改变这个值来起到同步器的作用,默认初始值为0,加一次锁state+1,释放一次锁state-1。在这里,state值的二进制数一共有32位,低16位供写锁使用,高16位供读锁使用
MAX_COUNT: 读写线程公用的属性,代表读或写能够加锁的最大次数。(读写都为65536)
SHARED_UNIT: 读线程的属性,这个属性的二进制数一共有32位,初始值一共有17位,低16位都为0,最高位为1,每次加读锁,state的高16位会改变;而低16位是属于写锁的,不会因为读操作改变
sharedCount: sharedCount方法为读线程的方法,方法传入的参数为原子值state,在这里取state的高16位,用来做一些判断,这个方法并不会改变state值
EXCLUSIVE_MASK: 是写线程的属性,为一个固定的二进制数,值为16个1
exclusiveCount: exclusiveCount方法为写线程的方法,方法传入的参数为原子值state,形参为c,c & EXCLUSIVE_MASK操作得出的结果一定是一个16位的二进制数,意味着为取state的低16位而不干涉它的高16位
总结:
这里,通过上述两个属性SHARED_UNIT、EXCLUSIVE_MASK和两个方法sharedCount、exclusiveCount将二进制数为32位的原子值state分为高16位和低16位,写锁占用低16位,读锁占用高16位,并且读写互不干涉
其它重要属性:
Sync的内部类HoldCounter :
//和ThreadLocalHoldCounter搭配使用,此类保存当前线程计数值count和线程id
//并作为threadlocal对象来使用,属于每个线程私有对象
static final class HoldCounter {
int count = 0;
// 使用线程id而不是线程对象的引用来避免GC
final long tid = getThreadId(Thread.currentThread());
}
//HoldCounter的一个对象
private transient HoldCounter cachedHoldCounter;
Sync的内部类ThreadLocalHoldCounter:
//此类继承了ThreadLocal类,并且让它的环形数组中存放的值为HoldCounter对象
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
//重写了initialValue方法,若首次调用get方法去取值,则将初值设置为HoldCounter对象
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//ThreadLocalHoldCounter对象
private transient ThreadLocalHoldCounter readHolds;
首个加锁成功读线程的属性:
//第一个成功获得读锁的线程
private transient Thread firstReader = null;
//是firstReader所持有的计数值
private transient int firstReaderHoldCount;
Sync构造方法:
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
tryAcquire和tryRelease方法,写锁lock和unlock的基石:
//tryAcquire方法和tryRelease方法与AQS的独享模式中的两个同名方法相似,tryRelease较简单,不作介绍
//tryAcquire和tryRelease都能被condition调用
//这里tryAcquire和tryRelease方法都是针对独享模式的,即写锁
protected final boolean tryAcquire(int acquires) {
/*
* 1. 如果读锁计数值不为0或写锁计数值不为0或锁的持有者不是当前线程,失败
* 2. 计数值超过最大限度,失败
* 3.成功
* /
Thread current = Thread.currentThread();
//c位原子值state,二进制数最大32位
int c = getState();
//exclusiveCount方法舍弃c的高16位,取低16位
int w = exclusiveCount(c);
//c不为0,有三种情况:高16位为0,低16位不为0;高16位不为0,低16位为0,或高低16位都不为0
if (c != 0) {
//因为c不为0, w为c的低16位,w=0意味着写锁没有被占有,但读锁已经被占用了;或者已经有其它写线程占用了锁,所以失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//超出计数范围
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//重入锁,锁的持有者位当前线程,并且当前线程再次尝试获锁
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
此方法执行的是尝试获锁操作,大致流程如下:
1、若已经有其它写线程占有了锁,则尝试获锁操作失败
2、若已经有读线程占有了锁,则尝试获锁操作失败
3、若当前占有写锁的线程再次尝试获取写锁则成功
可以看到,写锁的获锁方法和AQS中独享模式的获锁方法一样,同一时刻只能有一个写线程能够获取写锁,并且和读锁也会相互排斥,而且锁可以重入
tryRelease方法也和AQS中的方法类似,不做介绍
tryAcquireShared和tryReleaseShared方法,读锁lock和unlock的基石:
tryAcquireShared:
//这里传入的参数unused用来对原子值state进行修改,默认是1
protected final int tryAcquireShared(int unused) {
/*
* 1. 读锁已被线程持有,此方法失败
* 2. 否则尝试获取读锁
* 3. 第二步失败,在循环里重复步骤二
*/
Thread current = Thread.currentThread();
//c的二进制为32位
int c = getState();
//独享模式已经持有锁,并且不是当前线程,即有其它写线程加锁,则失败
//若是当前线程持有的写锁,则成功,即对同一个线程加读锁和写锁可以重入
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//取原子值c的高16位。即r的二进制只有16位数,值为c的高16位
int r = sharedCount(c);
//尝试用state+1,只不过是对state的高16位操作
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//刚开始没有线程持有锁,记住第一个加锁成功的线程,并将计数值加一
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//已经有一组线程获取了读锁,首个获取读锁的线程再次尝试加锁,它的计数值加一,即首个读线程加重入锁
firstReaderHoldCount++;
} else {
//rh为内部类HoldCounter的一个对象的引用,记录着当前线程的计数值和线程id
HoldCounter rh = cachedHoldCounter;
//因为并发问题导致两次获取的同一个线程id不相等
if (rh == null || rh.tid != getThreadId(current))
/*从threadlocal中取得相应的HoldCounter对象,这里有两种情况:
1、此线程第一次尝试加读锁,会调用ThreadLocal的实现类ThreadLocalHoldCounter重写的方法initialValue将此线程的
threadlocal的值设置为HoldCounter对象;
2、不是第一次,则直接从ThreadLocal中取出value
*/
cachedHoldCounter = rh = readHolds.get();
/*此线程第一次尝试加读锁,会调用ThreadLocal的实现类ThreadLocalHoldCounter重写的方法initialValue将此线程的
threadlocal的值设置为HoldCounter对象*/
else if (rh.count == 0)
readHolds.set(rh);
//加锁成功,计数值加一
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
tryAcquireShared大致执行流程:
1、有其它线程持有写锁,则操作失败
2、若当前尝试去获取读锁的线程正持有写锁(排他锁),此操作也能成功,这里其实也是锁的重入
3、每个读线程内部维护者一个threadlocal对象,此对象里存放着每个读线程加读锁次数的计数值count和线程id,若无写锁,则大部分读锁的加锁操作都能成功。
若有一组读线程尝试获读锁成功,则每个成功的读线程所持有的计数值count各加一,并且原子值也会加上相应的值;若同一线程多次加读锁,则此线程的count>1,也就是所谓的重入
4、其它情况,失败
5、若失败则将当前线程加入AQS中的等待队列,在一个自旋操作中再次尝试获锁(即调用AQS中的doAcquireShared方法)。要么之前线程释放锁,当前线程获锁成功;要么获锁失败,再次判定当前线程是否需要被阻塞
tryReleaseShared:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//若当前尝试释放锁的读线程是首个曾经成功获取读锁的线程
if (firstReader == current) {
//若计数值firstReaderHoldCount=1,证明第一个读线程只加了一次读锁
//释放它
if (firstReaderHoldCount == 1)
firstReader = null;
else
//计数值firstReaderHoldCount-1
firstReaderHoldCount--;
} else {
//否则获取当前线程的HoldCounter对象
HoldCounter rh = cachedHoldCounter;
//若HoldCounter对象为空或因为并发导致两次获取的同一个线程id不相等
if (rh == null || rh.tid != getThreadId(current))
//取得错误线程的HoldCounter对象
rh = readHolds.get();
//获取计数值
int count = rh.count;
//直接调用threadlocal的remove方法将此线程的HoldCounter删除
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//将目标线程的计数值减一
--rh.count;
}
//将对应的原子值state减一,然后判断释放锁操作是否成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
tryReleaseShared大致执行流程:
1、获取到需要释放锁的当前线程,然后通过此线程ThreadLocalHoldCounter对象取得计数值count,count- -
2、将原子值state减一
3、原子值不为0,意味着有其它线程持有着读锁,则操作失败;否则成功
4、若能成功释放,则调用AQS中的doReleaseShared方法去对等待队列做一些额外操作
它的公平和非公平性、中断暂且不看
具体使用方法:
public class ReadWriteLockDemo {
static class Util {
private int data;
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void get() {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "be ready to get data");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "get data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
public void put(int data) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " be ready to write data");
Thread.sleep(100);
this.data = data;
System.out.println(Thread.currentThread().getName() + " wrote data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
}
public static void main(String[] args) {
Util util = new Util();
for (int i = 1; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
while (true) {
util.get();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true) {
util.put(new Random().nextInt(10000));
}
}
}).start();
}
}
}