踩坑源于一道经典的多线程面试题: 用两个线程交替打印1~100的奇偶数
乍一看想解决方案还是不少的,比如暴力法直接判断线程名, 直接判断奇偶等,或者用通知等待方案。但是有没有一种简单有效的实现方法?设想了一下,利用公平锁的话,应该就很简单了,思路如下:
AB两个线程一起竞争公平锁,A如果先获得了锁, 那么B进行等待,A执行完成后,B的等待时间是最长的,那么B一定会获取本次的锁,依次类推,实现交替打印。
如何使用公平锁也是很简单,juc包下的ReentrantLock已经给我们留好了接口
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
就像上面源码所示,我们只需要在构造函数传入true,就会使用公平同步,构造函数的注释和我们设想的是一致的,if this lock should use a fair ordering policy 则传true就会采用公平排序策略。
看起来没啥毛病,动手写了一下实现:
private static final int COUNT = 100;
private static int start = 1;
static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
Runnable task = () -> {
for (; ; ) {
lock.lock();
try {
if (start <= COUNT) {
System.out.println(Thread.currentThread().getName() + "=> " + start++);
} else {
System.exit(0);
}
} finally {
lock.unlock();
}
}
};
new Thread(task).start();
new Thread(task).start();
}
代码看起来是比暴力法精简多了,输出一下,检证一下结果。 打印了五遍左右,大概没什么问题,但是第六次打印的结果,发现了一个细节,是有极小极小的几个情况,没有按照预期进行打印,如下:
pool-1-thread-1=> 1
pool-1-thread-2=> 2
pool-1-thread-1=> 3
pool-1-thread-2=> 4
......
pool-1-thread-1=> 55
pool-1-thread-2=> 56
pool-1-thread-1=> 57
pool-1-thread-2=> 58
pool-1-thread-2=> 59 ←※Error Print※
pool-1-thread-1=> 60
59这个数字按理说是thread-1去打印,但是thread-2却连续打了两条,这个情况就很让人困惑了,怕是用了假的公平锁吧?
又连续测试了几次,发现的确是有偶发这个情况,则去查阅了一下相关资料,一探究竟。
先看源码,公平锁比非公平锁的代码中,主要是多了一个判断条件,就是public final boolean hasQueuedPredecessors()大致为:维护了一个队列,如果有线程在排队了,则这次是轮不到你这个没排队的。
没什么问题,继续找,发现一段了doc中关于公平锁的这样的描述: > Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock.
docs.oracle.com/javase/8/do… 翻译过来则是: 但是请注意,锁的公平性不能保证线程调度的公平性。 因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。
答案自然就水落石出了。
官方文档和注释,是查阅源码的重要一环。自我盲目推测不可取!