公平锁 == 绝对有序?浅谈对Java公平锁使用的误区

1,653 阅读3分钟

踩坑源于一道经典的多线程面试题: 用两个线程交替打印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… 翻译过来则是: 但是请注意,锁的公平性不能保证线程调度的公平性。 因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。

答案自然就水落石出了。

官方文档和注释,是查阅源码的重要一环。自我盲目推测不可取!