当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,读-写和写-写互斥,提高性能。
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
}
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("获取读锁...");
r.lock();
try {
log.debug("读取");
sleep(1);
return data;
} finally {
log.debug("释放读锁...");
r.unlock();
}
}
public void write() {
log.debug("获取写锁...");
w.lock();
try {
log.debug("写入");
sleep(1);
} finally {
log.debug("释放写锁...");
w.unlock();
}
}
}
结果:读锁没有释放时,其他线程就可以获取读锁
09:29:07.134 c.DataContainer [t2] - 获取读锁...
09:29:07.134 c.DataContainer [t1] - 获取读锁...
09:29:07.134 c.DataContainer [t1] - 读取
09:29:07.134 c.DataContainer [t2] - 读取
09:29:08.146 c.DataContainer [t1] - 释放读锁...
09:29:08.146 c.DataContainer [t2] - 释放读锁...
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
//内部类 WriteLock类
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
//通过WriteLock类对象w调用该方法
public void lock() {
sync.acquire(1);
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
由于t1线程第第一个获取锁的线程,因此 t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//1、获取写锁当前的同步状态,即锁状态的低16位
int c = getState();
//2、获取写锁获取的次数
int w = exclusiveCount(c);
//如果写锁状态state!=0,说明写锁已经被其他线程获取
if (c != 0) {
//如果获取写锁的线程不是当前线程,获取写锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//写锁计数超过低 16 位, 报异常
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;
}
这里需要注意一点:int w = exclusiveCount(c);
表示写锁获取的次数
/**
该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,
结论:同步状态的高16位用来表示读锁被获取的次数。
*/
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/**
将同步状态(state为int类型)与0x0000FFFF相与,即获取同步状态的低16位,即写锁被获取的次数,
结论:同步状态的低16位用来表示写锁的获取次数。
*/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
总结:同步状态的低16位用来表示写锁的获取次数,同步状态的高16位用来表示读锁的获取状态。
当写锁已经被其他线程获取,就返回false,继续执行下面的逻辑。否则,获取锁成功并支持可重入锁,更新获取锁的次数。
该方法时Sync类中的抽象方法,有公平锁和非公平锁两种实现方式:
对于非公平锁:
static final class NonfairSync extends Sync {
//对于非公平锁总是返回false,不需要阻塞
final boolean writerShouldBlock() {
return false;
}
}
对于公平锁:
static final class FairSync extends Sync {
//对于公平锁,需要判断
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
}
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
public static class ReadLock implements Lock, java.io.Serializable {
//...
//调用读锁的lock()方法
public void lock() {
sync.acquireShared(1);
}
}
t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据并且获取写锁的线程不是当前线程,那么 tryAcquireShared 返回 -1 表示失败
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
public final void acquireShared(int arg) {
//tryAcquireShared 返回负数, 表示获取读锁失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
}
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)){
//获取锁成功
return 1;
}
//上面CAS获取读锁失败后,尝试循环获取
return fullTryAcquireShared(current);
}
这个方法对于公平锁和非公平锁的实现是不同的,也就导致了ReentrantReadWriteLock()对于公平和非公平的两种不同实现:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
}
对于readerShouldBlock()这个抽象方法,公平锁和非公平锁都有实现,具体实现有所不同。
对于非公平锁:
static final class NonfairSync extends Sync {
//...
final boolean readerShouldBlock() {
/**
看 AQS 队列中第一个节点是否是写锁,true 则该阻塞, false 则不阻塞:
由于非公平的竞争,并且读锁可以共享,所以可能会出现源源不断的读,使得写锁永远竞争不到,然后出现饿死的现象
通过这个策略,当一个写锁出现在头结点后面的时候,会立刻阻塞所有还未获取读锁的其他线程,让步给写线程先执行
*/
return apparentlyFirstQueuedIsExclusive();
}
}
对于公平锁:
static final class FairSync extends Sync {
//...
final boolean readerShouldBlock() {
//对于公平锁来说,如果有前驱(也就是非头结点),都会进行等待,不允许竞争锁
return hasQueuedPredecessors();
}
}
如果获取读锁获取失败,就会继续执行下面的doAcquireShared(arg)方法:想象成acquireQueued()方法
如果t2线程获取锁失败,这时会进入doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 。
在该方法中,t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁,如果获取锁没成功,在 doAcquireShared 内 for (; 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (; 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
注意:如果t2线程执行tryAcquireShared(arg)方法获取锁失败,那么总共会在doAcquireShared(arg)方法中执行3次doAcquireShared(1) 方法获取锁,如果还没有成功,就会进入阻塞状态
private void doAcquireShared(int arg) {
//将当前线程关联到一个 Node 对象上, 模式为共享模式,加入到同步队列的队尾
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//死循环,CAS自旋的方式尝试获取锁
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
//t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
if (p == head) {
int r = tryAcquireShared(arg);
//如果获取锁成功
if (r >= 0) {
//设置新的head节点,并(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//获取锁失败后是否应该被阻塞,如果需要阻塞,就调用 parkAndCheckInterrupt()方法阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
public static class WriteLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean release(int arg) {
//调用tryrelease()方法尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//如果头结点不为null并且waitStatus!=0 ,唤醒等待队列中下一个线程unpark()
unparkSuccessor(h);
return true;
}
return false;
}
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//因为可重入的原因, 写锁计数为 0, 才算释放成功
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果释放锁成功,就将加锁线程设置为null
setExclusiveOwnerThread(null);
//如果写锁计数不为0,更新写锁计数
setState(nextc);
return free;
}
这时会走到写锁的
sync.release(1)
流程,调用sync.tryRelease(1)
成功,变成下面的样子 :
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//将当前线程的节点状态置0
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点s
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//如果该节点已经取消获取锁,那就从队尾开始向前找,找到第一个ws<=0的节点,并赋值给s
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//调用unpark()方法,唤醒正在阻塞的线程
if (s != null)
LockSupport.unpark(s.thread);
}
接下来执行唤醒流程
sync.unparkSuccessor
,即让老二恢复运行:
这时 t2 在
doAcquireShared
内parkAndCheckInterrupt()
处恢复运行,这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
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) {
//2、继续尝试获取锁资源,让读锁计数加1
int r = tryAcquireShared(arg);
if (r >= 0) {
//3、唤醒下一个线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//1、t2线程在这儿被唤醒,就会继续指向一次for循环
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果锁计数>0,就继续唤醒下面的线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
//检查下一个节点是否是 shared,如果是将 head 的状态从 -1 改为 0 并唤醒老二
if (s == null || s.isShared())
doReleaseShared();
}
}
事情还没完,在
setHeadAndPropagate
方法内还会检查下一个节点是否是 shared,如果是则调用
doReleaseShared()
将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在doAcquireShared
内
parkAndCheckInterrupt()
处恢复运行
这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//省略不必要的代码...
//
for (;;) {
int c = getState();
//释放读锁
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
//计数为 0 才是真正释放
return nextc == 0;
}
}
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入
doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果头结点的waitStatus=Node.SIGNAL,就将其通过CAS改为0
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒下一个线程
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (; 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
由上面的源码可以看出,线程在获取读锁时,如果state!=0,那么会先判断获取写锁的线程是不是当前线程,也就是说一个线程在获取写锁后,还可以获取读锁,当写锁释放后,就降级为读锁了。