踩坑源於一道經典的多線程面試題: 用兩個線程交替打印1~100的奇偶數html
乍一看想解決方案仍是很多的,好比暴力法直接判斷線程名, 直接判斷奇偶等,或者用通知等待方案。可是有沒有一種簡單有效的實現方法?設想了一下,利用公平鎖的話,應該就很簡單了,思路以下:java
AB兩個線程一塊兒競爭公平鎖,A若是先得到了鎖, 那麼B進行等待,A執行完成後,B的等待時間是最長的,那麼B必定會獲取本次的鎖,依次類推,實現交替打印。面試
如何使用公平鎖也是很簡單,juc包下的ReentrantLock已經給咱們留好了接口api
/**
* 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就會採用公平排序策略。bash
看起來沒啥毛病,動手寫了一下實現:多線程
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 (; ; ) {
try {
lock.lock();
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();
}
複製代碼
代碼看起來是比暴力法精簡多了,輸出一下,檢證一下結果。 打印了五遍左右,大概沒什麼問題,可是第六次打印的結果,發現了一個細節,是有極小極小的幾個狀況,沒有按照預期進行打印,以下:oracle
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… 翻譯過來則是: 可是請注意,鎖的公平性不能保證線程調度的公平性。 所以,使用公平鎖的許多線程之一可能會連續屢次得到它,而其餘活動線程沒有進行且當前未持有該鎖。測試
看到這裏疑問就解開了,綜合官方的介紹,我的理解爲:公平鎖會盡量的保證鎖的公平,可是最後Thread scheduling(線程調度)是由JVM和操做系統去決定的,若是業務上須要有強順序性的話,不能依靠公平鎖。若是有大佬有詳細的看法,能夠在評論區告訴我。this
分割線------------------------------------------------------------------------------
到這裏其實還有一個小小的疑問,就是咱們的打印結果,大多數都是正確的,不正確的結果,也只是有不多的數字是錯誤的線程打印的。看起來公平鎖沒有理論上的那麼不靠譜,大膽的推測了一下爲何OS&JVM很是偶然的出現非預期的結果: (如下爲本人推測) 目前線程中鎖的代碼塊中,處理只有輸出數字這一個操做,處理時間極短,能夠忽略不計。這樣可能就致使了在某些極端的一個時刻,A線程執行完後,沒有及時的去獲取鎖,線程B就瓜熟蒂落的獲取了兩次鎖。 驗證一下本身猜測,將只有打印的代碼塊,增長了睡眠10ms的操做。
System.out.println(Thread.currentThread().getName() + "=> " + start++);
Thread.sleep(10);
複製代碼
輸出了屢次進行驗證,竟然和預期結果是正確的。 到這裏推測,個人猜想有多是正確的,可是既然官方文檔已經寫的很清晰了,仍是不要這麼去使用的好。