这里借用了 方腾飞老师<<JAVA并发编程艺术>>一节的 测试代码
以这个测试为入口,来从源码详细分析什么是公平,非公平,可重入锁
测试代码:
public class FairAndUnfairTest {
private static Lock fairLock = new ReentrantLock2(true);
private static Lock unfairLock = new ReentrantLock2(false);
private static CountDownLatch start;
public static void main(String[] args) {
FairAndUnfairTest test = new FairAndUnfairTest();
test.unfair();
}
public void fair() {
testLock(fairLock);
}
public void unfair() {
testLock(unfairLock);
}
private void testLock(Lock lock) {
start = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
Thread thread = new Job(lock);
thread.setName("" + i);
thread.start();
}
start.countDown();
}
private static class Job extends Thread {
private Lock lock;
public Job(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
try {
start.await();
} catch (InterruptedException e) {
}
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println("Lock by [" + getName() + "], Waiting by " + ((ReentrantLock2) lock).getQueuedThreads());
} finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public String toString() {
return getName();
}
}
private static class ReentrantLock2 extends ReentrantLock {
private static final long serialVersionUID = -6736727496956351588L;
public ReentrantLock2(boolean fair) {
super(fair);
}
public Collection<Thread> getQueuedThreads() {
List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
Collections.reverse(arrayList);
return arrayList;
}
}
}
公平性重入锁
调用fair()
并发5个线程,每个线程获取2次锁
输出:
Lock by [4], Waiting by []
Lock by [0], Waiting by [1, 2, 3, 4]
Lock by [1], Waiting by [2, 3, 4, 0]
Lock by [2], Waiting by [3, 4, 0, 1]
Lock by [3], Waiting by [4, 0, 1, 2]
Lock by [4], Waiting by [0, 1, 2, 3]
Lock by [0], Waiting by [1, 2, 3]
Lock by [1], Waiting by [2, 3]
Lock by [2], Waiting by [3]
Lock by [3], Waiting by []
公平性体现在,当前一个线程释放锁后,会从同步队列中取出第一个节点出来运行
源码分析:
public void run() {
try {
start.await();
} catch (InterruptedException e) {
}
for (int i = 0; i < 2; i++) {
// 公平性锁 fairLock
lock.lock();
try {
System.out.println("Lock by [" + getName() + "], Waiting by " + ((ReentrantLock2) lock).getQueuedThreads());
} finally {
// 释放锁
lock.unlock();
}
}
}
关键就是lock.lock和lock.lock
lock.lock -> acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 尝试去获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果为0则是第一次获取锁
if (c == 0) {
// 判断当前节点线程是否还有前置节点
// CAS设置state
// 设置拥有锁的线程是当前线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 当前线程就是占有锁的线程(锁重入state+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;
}
}
// 构造一个等待节点用于放入同步队列中
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;
// CAS 更新tail节点为当前节点
if (compareAndSetTail(pred, node)) {
// 为什么这里不放在node.prev上面?
// 因为并发情况下不知道是谁是尾节点所以只有在成功的情况下再设置
pred.next = node;
return node;
}
}
// 如果没有tail节点的话
enq(node);
return node;
}
// 更新头节点尾节点当前节点及他们之间的次序
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果没有tail节点
if (t == null) { // Must initialize
// CAS 更新当前节点为头节点
// 并且tail = head 都指向这一个节点(因为当前只有一个节点头尾都是它)
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 如果这个时候(并发线程的时候已经设置了tail节点)
// 把之前队列的最后一个节点设置为当前节点的前一个节点
node.prev = t;
// CAS更新尾节点,失败则for(;;)无限循环直到成功
if (compareAndSetTail(t, node)) {
// 前一个节点的下一个节点是当前节点
// 为什么这里不放在node.prev上面?
// 因为并发情况下不知道是谁是尾节点所以只有在成功的情况下再设置
t.next = node;
return t;
}
}
}
}
// 没有获取到锁的线程进入FIFO同步队列
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前一个节点
final Node p = node.predecessor();
// 如果前一个节点是头节点那么当前线程就是FIFO原则下一个
// 获取锁的线程,不断的尝试去获取锁
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);
}
}
// 这里如果不清楚 waitStatus值表示什么意识的去查下我就不列举出来了
// 是否应该park 删除无效线程节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 当前节点的前一个节点的状态
int ws = pred.waitStatus;
// 如果状态为 signal (唤醒)
if (ws == Node.SIGNAL)
/* 节点已经被设置为singal, 等于告诉上一个节点你线程跑完了唤醒我一下
* 所以它能安全的park
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/* 如果前一个节点被取消了(waitStatus 1: CANCELLED)
* 那么久进行检查将被取消的节点全部去掉直到节点状态<0
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* CAS 设置将前一个节点的等待状态值更新为 SIGNAL,
* 等于告诉上一个节点你线程跑完了唤醒我一下
* 因为这里CAS更新上一个线程状态值的时候,上一个线程还在运行它可能遇到各种情况导致
* waitStatus值改变,比如刚刚释放锁
* 如果更新失败就不Park了再次for(;;)
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// park 并且检查中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里一个完整的可重入公平锁就分析完了。
非公平性重入锁
调用unfair()
并发5个线程,每个线程获取2次锁
输出:
Lock by [4], Waiting by [0, 1, 2, 3]
Lock by [4], Waiting by [0, 1, 2, 3]
Lock by [0], Waiting by [1, 2, 3]
Lock by [0], Waiting by [1, 2, 3]
Lock by [1], Waiting by [2, 3]
Lock by [1], Waiting by [2, 3]
Lock by [2], Waiting by [3]
Lock by [2], Waiting by [3]
Lock by [3], Waiting by []
Lock by [3], Waiting by []
可以看到这里每个线程都获取了2次,证明了它并没有按照公平性原则
那么为什么每个线程都会获取2次呢?
我们来调整每个线程获取2次锁的代码
for (int i = 0; i < 2; i++) {
try {
Thread.sleep(100); // 新加入的代码
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("Lock by [" + getName() + "], Waiting by " + ((ReentrantLock2) lock).getQueuedThreads());
} finally {
lock.unlock();
}
}
输出:
Lock by [4], Waiting by [0, 2, 3, 1]
Lock by [0], Waiting by [2, 3, 1]
Lock by [2], Waiting by [3, 1]
Lock by [3], Waiting by [1]
Lock by [1], Waiting by []
Lock by [0], Waiting by [2, 1, 3]
Lock by [2], Waiting by [1, 3, 4]
Lock by [1], Waiting by [3, 4]
Lock by [3], Waiting by [4]
Lock by [4], Waiting by []
可以看到这里又公平了,我们来看看之前分析的tryAcquire代码
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 和公平锁的区别就在这里少了一个是否还有前一个节点的判断
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
相信大家都能看出来了原因了
在for循环中lock - unlock 这段代码期间锁还没有释放完成下一个循环又来了当前锁把state+1继续执行,所以会出现每个线程加锁2次。
因为加入了Thread.sleep(100)这个时间给锁释放完成,那么效果就会按照同步队列FIFO进行就相当于是公平锁了。
这里借用一张 <<JAVA并发编程艺术>>里面一张图来看看他们之间的性能对比
可以看到公平锁的耗时和上下文切换相比如非公平锁来说是它的很多倍。
但是公平锁可以有效防止饥饿线程