前面分析了并发工具类CountDownLatch和CyclicBarrier,本文分享分析比较重要的ReentrantReadWriteLock。
使用场景
以前的同步方式需要对读、写操作进行同步,读读之间,读写之间,写写之间等;工程师们发现读读之间并不会影响数据的一致性,完全可以不用同步。为了解决读读之间不阻塞,读写锁就诞生啦!写写和读写由于有写操作,会影响到数据的一致性的,因此他们之间需要阻塞。下面举了一个例子来说明ReentrantLock和ReadWriteLock之间的差别:
public class ReadWriteLockTest {
private static ReentrantLock lock = new ReentrantLock();
private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
// 读线程1
Runnable readThread1 = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始时间:" + System.currentTimeMillis());
// lock.lock();
rwLock.readLock().lock();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
rwLock.readLock().unlock();
}
System.out.println(Thread.currentThread().getName() + " 结束时间:" + System.currentTimeMillis());
}
};
// 读线程2
Runnable readThread2 = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始时间:" + System.currentTimeMillis());
// lock.lock();
rwLock.readLock().lock();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
rwLock.readLock().unlock();
}
System.out.println(Thread.currentThread().getName() + " 结束时间:" + System.currentTimeMillis());
}
};
new Thread(readThread1, "readThread1").start();
new Thread(readThread2, "readThread2").start();
}
}
运行结果:
ReentrantLock的结果
readThread1 开始时间:1539692006271
readThread2 开始时间:1539692006271
readThread1 结束时间:1539692008271
readThread2 结束时间:1539692010271
ReentrantReadWriteLock的结果
readThread1 开始时间:1539691790214
readThread2 开始时间:1539691790215
readThread1 结束时间:1539691792215
readThread2 结束时间:1539691792215
可见ReentrantLock锁的使用时间是4秒,也就是两个读线程的和;ReentrantReadWriteLock锁的使用时间是2秒,可见readThread1和readThread2两线程是并行的。
在读读不影响的情况下,ReentrantReadWriteLock优于ReentrantLock;因此当项目中读多于写时适合用读写锁。
那么ReentrantReadWriteLock是如何实现读写锁的呢?先来看一个读写锁分离的实例。
实例代码
定义两个读线程和一个写线程,启动顺序为:读1-写1-读2;
public class ReentrantReadWriteLockTest {
private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
private static Lock readLock = rwLock.readLock(); // 读锁
private static Lock writeLock = rwLock.writeLock(); // 写锁
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始:" + System.currentTimeMillis());
try {
readLock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "线程正在读文件...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
System.out.println(Thread.currentThread().getName() + "结束:" + System.currentTimeMillis());
}
}, "读1").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始:" + System.currentTimeMillis());
try {
writeLock.lock();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "线程正在写文件...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
System.out.println(Thread.currentThread().getName() + "结束:" + System.currentTimeMillis());
}
}, "写1").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始:" + System.currentTimeMillis());
try {
readLock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "线程正在读文件...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
System.out.println(Thread.currentThread().getName() + "结束:" + System.currentTimeMillis());
}
}, "读2").start();
}
}
运行结果:
读1开始:1539693400400
写1开始:1539693400401
读2开始:1539693400402
读1线程正在读文件...
读1结束:1539693401401
写1线程正在写文件...
写1结束:1539693403401
读2线程正在读文件...
读2结束:1539693404401
可以看出读1线程花费1秒,写1线程花费2秒,读2线程花费1秒,共4秒;
如果我把线程启动顺序改为:读1-读2-写1;运行结果为:
读1开始:1539693733410
读2开始:1539693733411
写1开始:1539693733411
读1线程正在读文件...
读2线程正在读文件...
读1结束:1539693734411
读2结束:1539693734411
写1线程正在写文件...
写1结束:1539693736411
可知读1和读2线程共花费1秒,写线程花费2秒,共花费3秒;说明在没有开启写线程之前,两个读线程认为是可以并行的,即使他们同时获得readLock锁。
源码分析
1 读写锁的构造方法
public ReentrantReadWriteLock() {
this(false); // this方法表明还有一个带有参数的构造方法
}
注意:说明同一个类的构造方法可以相互调用。
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
不带参数的构造方法默认调用带参的构造方法,参数为false。默认生成NonfairSync类型的对象。早在前面讲过抽象类Sync的两个实现类FairSync和NonfairSync,主要实现两个方法:
writerShouldBlock();
readerShouldBlock();
注意:这里通过判断线程是否按照公平的方式获得锁
来分为公平类和非公平类。这里所谓公平的方式是指:获取锁的线程是否需要按照先后请求锁的顺序获取锁
。看一下他们之间的继承关系:
本实例中生成的是非公平类的对象nonFairSync,同时生成ReadLock对象、WriteLock对象。
在这里,Lea大佬把读写锁分开实现,然后又通过ReentrantReadWriteLock把两者绑定在一起。
2 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);
}
// 可中断休眠状态的获取锁的方法,其他与lock()相同
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/* 只有当写锁没有被占有的时候才会获得读锁并返回true;即使采用的是公平策略;
* 如果写锁被其他线程占有则立即返回false;
*/
public boolean tryLock() {
return sync.tryReadLock();
}
// 等待读锁的线程等待timeOut时间,遵循公平策略;
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 释放锁,如果当前读锁线程数量为0,那么可以开始写锁的获取
public void unlock() {
sync.releaseShared(1);
}
// 读锁不支持条件 直接抛出异常
public Condition newCondition() {
throw new UnsupportedOperationException();
}
public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
通过上面源码可知:写锁空闲是读锁获取的前提(这点正好解释实例中读写锁获取顺序不同而执行时间不同);读锁的所有操作都是通过sync对象实现(sync对象分fairSync和NonfairSync)。
2.1 readLock.lock()
/* 在共享模式下请求读锁,并忽略中断。
* tryAcquireShard(arg)至少执行一次,根据返回值判断是否执行doAcquireShared(arg)
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
当tryAcquireShared(arg)返回值为-1(小于0)则需要阻塞当前线程,表明无法获得读锁;当返回值为1(大于0)时可以获得读锁。
/*
* 该方法用于尝试获取读锁:
* 若写锁被占有,则获取读锁失败;否则表明该线程可以获得读锁;
* 若是公平策略则判断是否等待,若无需等待则通过CAS操作更新state和count;
* 若获取读锁失败则采用循环重试机制;
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/*
* exclusiveCount(c)结果表示state中独占持有数,即写锁持有数
* 若独占持有数不为零且当前线程不是持有写锁的线程,则返回-1
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// r表示读锁持有的线程数量
int r = sharedCount(c);
// 如果写锁被占有或者读锁获取失败则通过循环重试获取读锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
tryAcquireShard(arg)返回-1的情况:
写锁被其他线程占有,返回-1,无法获得读锁;
tryAcquireShard(arg)返回1的情况:
-readerShouldBlock()返回true且读锁计数器在合理范围内,CAS更新state后,返回1,获得读锁;
注意:readerShouldBlock()根据NonfairSync和FairSync类不同,策略也不一样。FairSync判断该线程前面是否有等待队列,有则返回true,否则返回false;NonfairSync中,如果存在第一队列线程正在排他状态等待,则返回true. 如果这个方法返回true,并且当前线程正试图以共享模式获取,那么可以保证当前线程不是第一个排队的线程。
/*
* 进入该方法的前提是tryAcquireShard返回-1,即获取读锁需要等待
* 当前获取读锁的线程包装为一个共享模式的node,并阻塞当前线程
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过读锁的lock()方法可知:
-当写锁的线程被占有时,读锁获取失败;
-当线程因为CAS操作或者其他线程阻塞等原因导致读锁获取失败则会不断重试直到获取读锁;
2.2 readLock.unLock()
// 若读线程数为0,那么锁就可以用于写锁操作
public void unlock() {
sync.releaseShared(1);
}
/*
* 释放一个共享的读锁
* 如果tryReleaseShared()返回true则执行doReleaseShared()释放当前前程获得的读锁
* 如果tryReleaseShared()返回fasle,则不释放
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
具体看一下如何释放的:
// 返回true表明当前线程可以释放;返回false则不释放读锁
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 当前线程是否是第一个读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
// 否则holdCounter值减1
else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 自旋循环
for (;;) {
// 获取当前线程的状态值
int c = getState();
// 占有读锁的计数值减1
int nextc = c - SHARED_UNIT;
// CAS操作修改状态值,修改成功则返回nextc是否等于0;修改失败则循环
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
通过tryReleaseShard()方法确定是否要释放读锁以后,通过doReleaseShard()释放读锁.
private void doReleaseShared() {
// 自旋循环 用于唤醒下一个线程
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
读锁的释放比较简单,当前读锁数量减1并唤醒下一个等待的线程;
3 writeLock
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;
}
/**
* 请求写锁
* 写锁或者读锁都处于空闲状态,那么获取成功;并设置写锁计数器为1
* 如果当前线程已经持有写锁,那么获取成功,并写锁计数器加1
*
* 如果其他线程占据写锁,则当前请求的线程被阻塞,直到获取写锁
*/
public void lock() {
sync.acquire(1);
}
/**
* 与lock方法类似,但能中断被阻塞的线程,阻塞状态也会被清除;
* /
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 只有当写锁没有被其他线程占有才能获取写锁,即使是公平策略。
* 但当前线程已经持有该写锁,因为是可重入的,所以也能成功;
*/
public boolean tryLock( ) {
return sync.tryWriteLock();
}
/**
* 当①写锁在指定时间内一直处于空闲且②当前线程没有被阻塞,则获取写锁成功;
* 如果线程采用公平类获取写锁,则需要排队获取;这与tryLock()不一样;
*
* 如果当前线程已经持有写锁,则获取锁成功;
*
* 如果写锁被其他线程占有,则当前线程被阻塞直到三件事发生:①获得写锁,停止阻塞;②其他线程打断当前线程,停止阻塞;③到达指定等待时间,停止阻塞;
*
* 总结:返回true的情况:①当读锁和写锁都处于空闲且被当前线程获取;②当前线程已经持有该写锁;返回false的情况:在指定时间内没有获得写锁;
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
* 释放锁的方法:
* ①若当前线程持有写锁,则写锁计数器减1;计数器为0则释放写锁;
* ②若当前线程并不持有写锁则报异常错误并抛出;
*/
public void unlock() {
sync.release(1);
}
/**
* 返回一个condition实例
* condition实例和object对象的wait() notify()功能类似
* <ul>
* 当condition对象的方法调用,写锁还处于空闲状态则报异常;
*
* 当condition对象的await()方法被调用,写锁被释放;且该线程等待写锁
*
* 正在等待的线程被其他线程中断,则等待终止并抛出中断异常,线程的中断状态被清除;
*
* 等待的线程采用先进先出的策略获取写锁
*/
public Condition newCondition() {
return sync.newCondition();
}
public String toString() {
Thread o = sync.getOwner();
return super.toString() + ((o == null) ?
"[Unlocked]" :
"[Locked by thread " + o.getName() + "]");
}
/**
* 当前线程是否持有写锁
*/
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
/**
* 当前线程持有锁的数量
*/
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
写锁比读锁多了两个方法,用于判断当前线程是否写锁以及计算当前线程持有锁的数量。
3.1 writeLock.lock()
/*
* 若读锁和写锁都空闲,当前线程立马获取写锁,并设置写锁计数器为1;
* 若当前线程已经持有写锁,则设置写锁计数器+1;
* 若当前写锁或者读锁已经被其他线程占有,则阻塞当前线程直到获取写锁;
*/
public void lock() {
sync.acquire (1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)返回true表明获得写锁,后面两个方法则不执行;返回false表明无法获得写锁,执行acquireQueued(把当前线程包装成独占节点并放入等待队列)和selfInterrupt(线程阻塞);
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 写锁的独占数
int w = exclusiveCount(c);
if (c != 0) {
// 读锁被线程占有,或者写锁被其他线程占有,则返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 写锁被当前线程持有,更新写锁计数器超过最大数,抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 这里表明写锁被该线程已经持有情况下更新state并返回true
setState(c + acquires);
return true;
}
// 这里表
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
tryAcquire(arg)返回false的情况:
- 读锁被线程占有,则返回false;(不管是哪个线程占有)
- 写锁被其他线程占有,则返回false;
- 写锁数量已经超过最大抛出异常,返回false;
- writeShouldBlock()为true,则返回false;
- CAS操作失败,则返回false;
tryAcquire(arg)返回true的情况:
- 读锁和写锁都空闲,则返回true;(返回前CAS更新state,并设置读锁的独占线程)
- 仅写锁被当前线程占有,则返回true;(返回前更新state值)
- writeShouldBlock()返会false,则返回true;(返回前CAS更新state,并设置读锁的独占线程)
注意:这里的writeShouldBlock()
返回值根据FairSync和NonfairSync实现而定;NonfairSync类的writeShouldBlock()直接返回false
,表明非公平类的写锁不需要阻塞;FairSync类的writeShouldBlock()根据队列里面是否有其他线程等待写锁来返回true和false
;
当tryAcquire(arg)返回true表明获得写锁,方法acquire(arg)结束;否则继续执行acquireQueued(…);
首先通过addWaiter(Node.EXCLUSIVE)方法把当前包装成一个独占的节点,并放置等待队列的尾部;
acquireQueued(node,arg)方法作用是使得已在队列的线程获得排他不可中断模式,用于条件等待方法和获取方法。
selfInterrupt()则用于阻塞线程。
3.2writeLock.unLock()
/*
* 如果当前线程持有该锁,则锁的计数器减1;若计数器为0,则释放锁
* 当前线程没有持有该锁则抛出异常
*/
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
根据tryRelease(arg)的返回值(true/false)判断是否要释放锁。
protected final boolean tryRelease(int releases) {
// 当前线程不是写锁的独占线程则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 写锁计数器是否为0,为0则释放锁,否则不释放锁
boolean free = exclusiveCount(nextc) == 0;
// 如果写锁的独占计数器为0则清除独占线程
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
根据上面的源码可知,当写锁的独占计数器为0则返回true,表示可以释放写锁;否则不释放。
简单总结
至此,分析了读锁和写锁在获得和释放两个方法的源码分析,可知读锁和写锁的获取有以下几点不同:
- 获取写锁的条件比较苛刻,而读锁则比较宽松;
- 读锁是一种shard锁,而写锁是Exclusive锁;即读锁是可以共享的,多个线程可以同时获得,而写锁是独占的,只能由一个线程获得(体现在独占线程变量);
- 读锁释放的过程是读锁计数器减1并唤醒下一个等待线程;而写锁的释放必须要写锁计数器为0才能释放写锁(这也体现独占性);
参考资料
《实战java高并发程序设计》
https://blog.csdn.net/qq_19431333/article/details/70568478