证明
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("acquire read lock 1");
lock.readLock().lock();
System.out.println("get read lock 1");
sleep(200);
lock.readLock().unlock();
System.out.println("release read lock 1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sleep(100);
System.out.println("acquire write lock 1");
lock.writeLock().lock();
System.out.println("get write lock 1");
sleep(400);
lock.writeLock().unlock();
System.out.println("release write lock 1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sleep(300);
System.out.println("acquire read lock 2");
lock.readLock().lock();
System.out.println("get read lock 2");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sleep(400);
System.out.println("acquire write lock 2");
lock.writeLock().lock();
System.out.println("get write lock 2");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sleep(500);
System.out.println("acquire read lock 3");
lock.readLock().lock();
System.out.println("get read lock 3");
}
}).start();
这段代码陆续加了5把锁,加减锁的顺序分别是:
- 加1号写锁
- 加1号读锁
- 加2号写锁
- 加2号读锁
- 释放一号写锁
我们来预测一下这五把锁的获取顺序是怎样的
首先读写锁并没持有任何锁,因此1号写锁成功获取锁,
接下来2,3,4步由于读写锁已经持有写锁,因此后三把锁只能等待
接下来释放1号写锁,基于公平锁的原则,排在最前面的1号读锁会马上获取锁
而根据只要有一把写锁,别的写锁也能成功加锁的原则,
2号写锁也会成功加锁,
这时只有2号写锁还在等待状态。
然而事实是这样的吗?
acquire write lock 1
get write lock 1
acquire read lock 1
acquire write lock 2
acquire read lock 2
release write lock 1
get read lock 1
以上为控制台的输出结果,可见在1号写锁释放锁之后,只有1号读锁成功获取了锁,而2号读锁只有等着。
这显然是违反公平锁的原则,若干在1号锁获取锁成功后,别的线程来加读锁,无疑是会成功的,而2号读锁只能傻傻的等着,这不是欺负老实人吗。
所有这是JDK的逻辑bug!
原因
让我们读读源码看看为啥会这样
/**
* Acquires the read lock.
*
* <p>Acquires the read lock if the write lock is not held by
* another thread and returns immediately.
*
* <p>If the write lock is held by another thread then
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until the read lock has been acquired.
*/
public void lock() {
sync.acquireShared(1);
}
首先获取写锁时,会调用AQS的acquireShared方法
/**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
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);//(1)
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()//(2))
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
而acquireShared方法会调用doAcquireShared,当一个线程获取到读锁的时候会调用setHeadAndPropagate方法
/**
* Sets head of queue, and checks if successor may be waiting
* in shared mode, if so propagating if either propagate > 0 or
* PROPAGATE status was set.
*
* @param node the node
* @param propagate the return value from a tryAcquireShared
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
setHeadAndPropagate方法很简单,它会尝试去唤醒队列中下一个的处于Shared状态的节点。而下一个节点被唤醒时,它应该处于doAcquireShared的parkAndCheckInterrupt这个位置。它进而也会调用setHeadAndPropagate,来唤醒下一个Shared状态的节点。这样一直唤醒,直到遇见了一个Excluse状态的节点,这种连锁唤醒行动就停止了。
Shared1 ->Shared2 ->Shared3 ->Exclusive1->Shared4
如上队列Shared1引发的连锁唤醒便不能唤醒Shared4,因此导致了我们观察到的非公平现象。
为什么会这样
这并不会造成严重的后果,之所以JDK会采用如下的实现方式,主要是由于AQS的局限性。因为AQS并不能提供后排插到前排的机制。AQS的队列调度使用了CAS,因此仅仅修改队首和队尾时只用考虑一个变量,而从后排到前排则复杂的多,首先从后排删除同时要影响前后两个节点,而插入前排同样会影响两个节点。而如果想让exclusive后排的节点被唤醒,那么就需要便利整个队列,这是需要耗时的,CAS没法保证四个变量同时被修改,若在遍历途中读锁全释放了,那遍历如何取消?若遍历途中读锁被释放又加上,怎么保证同时只有一个遍历在进行?
或许解决的办法只有加锁,AQS的state被用来存储同时被加的锁的数量,而当state=0时一般才能释放锁。那么只要在遍历开始时增加state,那么就能保证读锁在遍历结束前不会释放。
但是这样做并不是解决之道,首先在AQS这种锁底层框架中用到锁本身就很影响性能,其次state是AQS面向它的继承者的一个方法,对AQS本身没有意义,如果将state和AQS依赖在一起,那么从逻辑上来说缺乏整体性,并且可能会影响别的基于AQS实现的同步工具,这是得不偿失的。
结论
ReentrantReadWriteLock不是公平锁,但是世界上哪有绝对的公平。社会总是在效率和公平中做出折中的选择,而JDK也是,这或许就是宇宙的哲学!
网友评论