Lock锁<二> _ 重入锁/读写锁
目录一、重入锁ReentrantLock1. 实现锁重进入2. 获取公平锁与非公平锁的区别二、读写锁ReentrantReadWriteLock1.ReentrantReadWriteLock的API2. 读写状态设计3. 写锁的获取与释放4.读锁的获取与释放5. 写锁降级三、参考资料一、重入锁ReentrantLock重入锁指线程获得锁后能够再次获取该锁而线程不被阻塞。synchronized关
目录
一、重入锁ReentrantLock
重入锁指线程获得锁后能够再次获取该锁而线程不被阻塞。synchronized关键字隐式的支持重进入。java.util.concurrent.locks.ReentrantLock类是重入锁,实现Lock接口。ReentrantLock不仅支持锁的重入,而且还支持获取锁的公平和非公平选择。
公平的获取锁是指等待时间最长的线程最先获取锁,即:锁按FIFO顺序获取。ReentrantLock默认是非公平锁,原因:公平锁虽然保证锁的获取按照FIFO原则,而代价是进行大量的线程切换;非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证其更大的吞吐量。
1. 实现锁重进入
以下代码是非公平锁的获取和释放,需要注意问题:
- 线程再次获取锁:已获取锁的线程是否是当前线程,若是则再次成功获取锁;
- 锁的最终释放:成功获取锁,state都要+1,即:state值表示当前被重复获取的次数,而释放锁时,则state自减,最后等于0时表示锁已经成功释放。
/**
* 非公平性获取锁
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0,表示没有任何线程获得锁
if (c == 0) {
// 尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state != 0,说明已有线程获取锁
// 判断当前线程 == 已获取锁的线程,则再次加锁,计数器+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* 非公平锁释放
*/
protected final boolean tryRelease(int releases) {
// 每次释放,state自减
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 0时,表示锁成功释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2. 获取公平锁与非公平锁的区别
公平锁按照FIFO队列的顺序获取锁,在非公平锁的基础上增加hasQueuedPredecessors()条件判定,即:同步队列的当前节点是否有前节点,若有返回true。
- 非公平锁的获取:compareAndSetState(0, acquires)
- 公平锁的获取:!hasQueuedPredecessors() && compareAndSetState(0, acquires)
/**
* 公平锁的获取
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0,表示没有任何线程获得锁
if (c == 0) {
// 尝试获取锁
// 公平获取锁 = 是否有前节点 + 非公平获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state != 0,说明已有线程获取锁
// 判断当前线程 == 已获取锁的线程,则再次加锁,计数器+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
二、读写锁ReentrantReadWriteLock
读写锁是指同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个读锁(共享锁)和一个写锁(排他锁)。同时读写锁也支持公平或非公平锁、锁重入、写锁降级。
java.util.concurrent.locks.ReentrantReadWriteLock读写锁实现ReadWriteLock接口,维护了一个读锁、一个写锁。注意读写锁的性能比排他锁的性能好,大多数场景下读多于写,这样多线程读提高了并发量和吞吐量。
1. ReentrantReadWriteLock的API
注意:getReadLockCount(),获取当前读锁被获取的次数(共享读次数 + 重入锁次数);getReadHoldCount(),获取当前线程获取读锁的次数(一个线程的重入锁次数),存储在每个线程的ThreadLocal下。
2. 读写状态设计
同步器AQS维护一个int型的state变量,即:同步状态(private volatile int state) 。如下图所示,这个state同步状态“按位切割”:高16位,表示读锁;低16位,表示写锁。
根据state值划分能得出一个推论:state不等于0时,当写状态(state & 0x0000FFFF)等于0时,则读状态(state >>> 16)大于0,即读锁已被获取。
3. 写锁的获取与释放
如以下代码是java.util.concurrent.locks.ReentrantReadWriteLock.Sync类中方法,定义了写锁的获取,也支持写锁重进入。当前线程已获取写锁,则增加写状态;如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者当前线程不是已获取写锁的线程,则当前线程进入等待状态。
/**
* ReentrantReadWriteLock写锁的获取
* 该方法在java.util.concurrent.locks.ReentrantReadWriteLock.Sync继承了AQS
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取state值
int c = getState();
// 获取写状态
int w = exclusiveCount(c);
// state值 != 0
if (c != 0) {
/*
* state值 != 0 且 写状态 == 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;
}
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0
时表示写锁已被释放。
4. 读锁的获取与释放
如以下代码是java.util.concurrent.locks.ReentrantReadWriteLock.Sync类中方法,定义了读锁的获取,也支持读锁重进入。如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
注意:其他线程没有持有写锁,但是当前线程已有写锁,那么读锁也可以成功获取,因为存在写锁降级的过程,详见下小节《写锁降级》。
注意:读状态是所有线程获取读锁次数的总和,由getReadLockCount()获取;而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由getReadHoldCount()获取。
/**
* ReentrantReadWriteLock读锁的获取
* 该方法在java.util.concurrent.locks.ReentrantReadWriteLock.Sync继承了AQS
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
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 {
ReentrantReadWriteLock.Sync.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);
}
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的
值是(1<<16)。
5. 写锁降级
写锁降级指的是写锁降级成为读锁。锁降级是指当前线程已拥有写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。但是当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称锁降级。同时不支持锁升级。下面是写锁降级的代码实例。
// 创建读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获取写锁
Lock writeLock = readWriteLock.writeLock();
// 获取读锁
Lock readLock = readWriteLock.readLock();
@Test
public void writeLockTest(){
// 写锁降级从写锁获取开始
writeLock.lock();
try {
// 再次获取读锁
readLock.lock();
} finally {
// 释放写锁
writeLock.unlock();
}
// 写锁降级完成,降级为读锁
}
锁降级中读锁的获取是否必要呢? 答案是必要的。主要是为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
三、参考资料
Lock锁<一> _ 基础_爱我所爱0505的博客-CSDN博客
ReentrantLock基础知识_米兰的小铁匠z的博客-CSDN博客_reentrantlock
更多推荐
所有评论(0)