ReentrantReadWriteLock
干啥的?
可重入的读写锁
读写锁是干啥的?有什么用?和互斥锁有何区别?为什么会有读写锁的存在?它解决了什么问题?它是如何实现的?
将原来的互斥锁,切割为了两把锁:读锁+写锁
为什么要切割?
考虑一下,三个线程:A、B、C,其中线程A和B为读取数据的线程,而C为修改数据的线程,且C很少去修改数据,此时为什么要让A和B争用互斥锁呢?因为此时它俩并不修改数据,所以同时并发读取是没有任何问题的,所以我们把锁切割为了两把,当读时,获取读锁,写时获取写锁,且读锁可以多个线程同时获取,此时我们称读锁为共享锁,写锁为互斥锁。
我们自己实现一个读写锁
问题一:如果两个线程同时去拿读锁和写锁,因为两个判断不是原子的,所以都有可能进去if判断,那么两个线程就分别拿到了读写锁。所以我们优化了程序,添加了第三个变量。虽然保障了原子性,但是读锁变为了互斥锁。
问题二:读锁不能共享。
int state;
32位的变量,此时我们考虑切割为高16位+低16位,分别用于表示读锁和写锁,此时由于只需要操作一个变量,所以只需要一次CAS即可,不需要保证多个操作的原子性。
怎么解决写锁饥饿问题?
在获取读锁之前,判断当前有没有写锁排队即可
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
}
为什么读写锁还有公平锁和非公平锁的实现?
这里的公平和非公平是对写锁而言的。
如果一个线程已经持有了读锁,那么此时再来一个写锁,就会放到等待队列,后面再来了读锁就会继续往后排队,此时又来了一个写锁,如果是公平的,它会看一下前面有没有写锁在排队,如果是非公平的,它会直接试着加写锁。
为什么非公平锁性能高于公平锁?
线程上下文切换时间+调度延迟时间
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
}
读锁和写锁都是拿的同一个同步器(Sync),这个同步器继承了AQS类,实现了大部分的逻辑。读共享,写互斥都由它来保证。
如果我想要知道当前线程获取了多少次共享锁,也即重入了多少次共享锁怎么办?因为state的高16位是所有读线程共享的。通过ThreadLocal来记录每个线程获取了多少次共享锁即可,所以我们称state的高16位用于存储所有读线程获取共享锁的次数,TL用于表示当前线程自己的重入次数,sum(all thread tl count)= state >>16
假如所有时间都是同一个线程获取读锁,那么有没有必要使用TL?因为TL占用内存,没必要,所以我们做一个优化,在读写锁中维护一个:firstCount,保存第一个线程对象和获取锁的数量即可
abstract static class Sync extends AbstractQueuedSynchronizer {
//最后一个线程获取锁的次数,最后一个线程有可能成为第一个
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
//获取一个volatile的变量,保证之前CPU缓存的值刷到主存
setState(getState()); // ensures visibility of readHolds
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//当前写锁的count不为0,并且还不是当前线程持有的写锁,解决线程饥饿问题
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//读锁应不应该阻塞,如果是非公平锁,那么就会看队列中是否有写锁在排队
if (!readerShouldBlock() &&
//判断r如果小于MAX_COUNT,那么读锁的count++,表示获取到了读锁
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果r=0,表示当前线程是第一个获取读锁的线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//如果r!=0,判断当前线程是不是第一个
firstReaderHoldCount++;
} else {
//不是第一个,那么就获取最后一个线程
HoldCounter rh = cachedHoldCounter;
//判断当前线程是不是最后一个线程,如果不是,那么就申请一个ThreadLocal,然后++
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);
}
}
第21~24行的三个判断如果是false,下面对应着处理代码
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//道格李的一贯做法,如果多线程下无锁,必定会使用死循环
for (;;) {
int c = getState();
//已经有线程持有互斥锁,并且还不是当前线程
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
//第21~24行的三个判断如果是false,下面对应着处理代码
else if (readerShouldBlock()) {//读锁应该被阻塞
if (firstReader == current) {
//如果当前线程就是第一个读线程,那么啥也不做
} else {
//如果不是第一个,那么就对线程的ThreadLocal变量进行操作
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
//表明没有获取读锁,返回-1,阻塞当前线程
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//和tryAcquireShared中一样
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
进来的时候判断,如果有互斥锁,那么直接退出,没有的话,判断该线程要不要阻塞,如果需要阻塞,那么清除一下它的ThreadLocal,如果不需要阻塞,先对state进行+1,然后对自身的count进行++,加的时候分为几种情况,如果是第一个线程,那么直接firstReaderHoldCount++,如果不是,那么对ThreadLocal中的变量进行++
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false;
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} 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();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//读锁释放完毕之后,看看等待队列中有没有排队的线程,如果有就唤醒
return nextc == 0;
}
}
public static class WriteLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
//Sync的release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
}
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
AQS
如何保证三个操作的原子性?
可以加锁,但是这样会导致性能严重下降,那么此时只能通过CAS操作来保证多线程操作的安全性,但是CAS只能保证一个操作的安全,那么这三个操作应该先保证哪个呢?
特别注意,当上面CAS成功后,有一瞬间,这里的pred.next并没有关联,会导致什么问题?
有一瞬间,你通过head进行遍历的时候,是到达不了最后一个节点的!
如何获取最新的节点呢?
通过tail指针往前遍历即可。
addWaiter方法使用优化前置,把enq里的代码移到了前面
static final class Node {
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//特别注意,当上面CAS成功后,有一瞬间,这里的pred.next并没有关联
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
当加入阻塞队列后,调用该方法考虑是否将当前线程进行阻塞,在看该方法时,请考虑一个情况:
假如在添加到阻塞队列后,当前状态时无锁时,怎么办?那么一定是尝试获取锁。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱节点的状态是SIGNAL,那么可以安心睡眠,因为SIGNAL状态代表了上一个线程是
//活的,它可以通知你
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || //为什么这里有可能为空?因为我们首先更新的是tail引用,然后才是
//t.next = node; 有可能一瞬间为空
s.waitStatus > 0 //后继节点居然是无效节点?因为上面第32行更新的时候不是原子的,
//所以有可能一瞬间指向的仍然是无效节点
) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
共享锁获取锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//获取锁失败,进入此方法,获取锁的逻辑由子类自己来实现
doAcquireShared(arg);
}
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);
}
}
此时有一个场景:
Semaphore s = new Semaphore(2);
线程A,B去acquire,此时AB都获取到了信号量,再来两个线程C,D此时就会去队列中排队。
当A调用了release释放了信号量,此时唤醒了C,C执行完tryAcquireShared(arg);(上面第15行代码)后,此时
r=0,代码继续往下走,走到setHeadAndPropagate(node, r);时,此时node=C,r=0,然后进入setHeadAndPropagate(C , 0),
不会唤醒D,因为r=0,就在此时,线程B调用了release,释放了信号量,此时就会出现明明还有信号量,但是D没有被唤醒。
A,B(获取到信号量),队列中是C->D,此时A释放信号量,唤醒了C,
用于更新头节点,并且唤醒后继共享节点,
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //老的头节点
setHead(node);
if (propagate > 0 ||//信号量还有多余的,那么直接唤醒
h == null || //不可能发生
h.waitStatus < 0 ||// SIGNAL 表明必须唤醒后继节点
(h = head) == null || //不可能发生
h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;//保存head临时变量,因为执行下面的代码时可能被其他线程改过了
if (h != null && h != tail) {//链表中还存在等待节点
int ws = h.waitStatus;
//理应要唤醒后面的节点,因为SIGNAL的语义就是必须唤醒后面的节点(正常状态)
if (ws == Node.SIGNAL) {
//CAS将该状态由SIGNAL改为0,表示唤醒成功,那么如果失败呢?
//失败的场景就是两个线程同时释放了资源,同时唤醒后面的节点
//比如A,B两个线程同时调用了Semaphore.release(),队列中head ->C -> D
//此时这里只有一个线程可以
//执行成功,例如A成功了,将旧的头节点的waitStatus更新成了0,那么B就会
//失败,就会再次进入循环,此时head有两种可能,一种是被更新成了C,
//因为C被unpark唤醒之后会进到上面代码第12行的for中,此时队列中没变,
//但是走到 setHead(node);时,将head更新成了C,此时
//C的ws == Node.SIGNAL,所以会正常走if,【还有一种可能,就是还没执行到
//setHead(node),此时还是旧的空节点,waitStatus还是0,】
//就不会再走这个if,而是走else,此时就会将空的head的waitStatus更新为
//Node.PROPAGATE,此时就会进入if (h == head) ,此时如果head还没更新为C
//就会一直自旋,直到更新成了C,然后break,此时代码从setHead(node)继续
//执行,因为Node h = head;此时还是空的头结点,所以会进入h.waitStatus < 0
//此时空的头结点的状态是Node.PROPAGATE(-3),就会进入if循环,此时Node h = head
//已经更新成了C,C就会唤醒D;我们回想一下,如果没有下面这个修改成PROPAGATE的步骤,那么
//回到上面大括号的步骤,此时如果head还没更新为C就break了,此时代码从setHead(node)
//继续执行,此时因为h.waitStatus=0,所以不会进入if循环,直接就返回了,不会再唤醒D,就会出现
//明明还有信号量,但是等待的线程却不能被唤醒
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒h.next
unparkSuccessor(h);
}
//代码走到这,就表明A释放了一个信号量,唤醒C,在C还没执行到setHead(node)时
//又有一个信号量释放了,
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
Semaphore的acquire就是CAS-1操作
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
Semaphore的release就是CAS+1操作
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
acquireInterruptibly和acquire的区别就是acquireInterruptibly可以被unpark和中断唤醒,但是acquire只能被unpark唤醒
这里的自旋有个阈值,spinForTimeoutThreshold
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}