簡要概述線程如何交互的細節。之前,我們研究了線程是如何相互同步的。這次我們將深入探討線程交互時可能出現的問題,並討論如何避免這些問題。我們還將提供一些有用的鏈接以供更深入的研究。
安裝了 JVisualVM 插件(通過工具 -> 插件),我們可以看到死鎖發生的位置:
根據 JVisualVM,我們看到了休眠期和停放期(這是當線程試圖獲取鎖時——它進入停放狀態,正如我們之前討論線程同步時所討論的)。您可以在此處查看活鎖示例:Java - 線程活鎖。
你可以在這裡看到一個超級例子:Java - Thread Starvation and Fairness。此示例顯示線程在飢餓期間會發生什麼,以及從 到 的一個小更改如何
問題的一部分添加到 IntelliJ IDEA 中,該問題在2010 年的 發行說明中列出。

介紹
所以,我們知道Java有線程。您可以在標題為Better together:Java 和 Thread 類的評論中閱讀相關內容。第一部分 — 執行線程。我們在標題為Better together:Java 和 Thread 類的評論中探討了線程可以相互同步這一事實。第二部分 — 同步。是時候討論線程如何相互交互了。他們如何共享共享資源?這裡可能會出現什麼問題?
僵局
最可怕的問題是死鎖。死鎖是指兩個或多個線程永遠在等待另一個線程。我們將從描述死鎖的 Oracle 網頁中獲取示例:
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
第一次可能不會在這裡發生死鎖,但是如果您的程序確實掛起,那麼就該運行了jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
線程 1 正在等待線程 0 的鎖。為什麼會發生這種情況?Thread-1
開始運行並執行該Friend#bow
方法。它被標記為關鍵字,這意味著我們正在為(當前對象)synchronized
獲取監視器。this
該方法的輸入是對其他對象的引用Friend
。現在,Thread-1
想要在另一個上執行該方法Friend
,並且必須獲取它的鎖才能這樣做。但是如果另一個線程(在本例中Thread-0
)設法進入該bow()
方法,那麼鎖已經被獲取並Thread-1
等待Thread-0
,反之亦然。這是無法解決的僵局,我們稱之為僵局。死鎖就像一個無法鬆開的死神之握,是一種無法打破的相互阻礙。關於死鎖的另一種解釋,可以看這個視頻:死鎖和活鎖詳解。
活鎖
如果有死鎖,是否也有活鎖?是的,有 :) 活鎖發生在線程表面上看起來還活著,但它們無法做任何事情時,因為它們繼續工作所需的條件無法滿足。基本上,活鎖類似於死鎖,但線程不會“掛起”等待監視器。相反,他們永遠在做某事。例如:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
此代碼的成功取決於 Java 線程調度程序啟動線程的順序。如果Thead-1
先開始,那麼我們會得到活鎖:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
從示例中可以看出,兩個線程依次嘗試獲取兩個鎖,但都失敗了。但是,他們並沒有陷入僵局。從表面上看,一切都很好,他們正在做自己的工作。 
飢餓
除了死鎖和活鎖,多線程還有一個問題:飢餓。這種現像不同於以前的阻塞形式,因為線程沒有被阻塞——它們只是沒有足夠的資源。結果,雖然一些線程佔用了所有執行時間,但其他線程無法運行:
https://www.logicbig.com/
Thread.sleep()
讓Thread.wait()
您平均分配負載。 
競爭條件
在多線程中,存在“競爭條件”這樣的東西。當線程共享資源時會發生這種現象,但代碼的編寫方式無法確保正確共享。看一個例子:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
此代碼第一次可能不會產生錯誤。當它出現時,它可能看起來像這樣:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
如您所見,在newValue
賦值時出了點問題。newValue
太大了。value
由於競爭條件,其中一個線程設法更改了兩個語句之間的變量。事實證明,線程之間存在競爭。現在想想不在貨幣交易中犯類似的錯誤是多麼重要......示例和圖表也可以在這裡看到:Code to simulate race condition in Java thread。
易揮發的
說到線程的交互,這個volatile
關鍵詞值得一提。讓我們看一個簡單的例子:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
最有趣的是,這很可能不起作用。新線程將看不到字段中的更改flag
。要為該字段修復此問題flag
,我們需要使用關鍵字volatile
。如何以及為什麼?處理器執行所有操作。但是計算結果必須存儲在某個地方。為此,有主內存和處理器的緩存。處理器的高速緩存就像一小塊內存,用於比訪問主內存更快地訪問數據。但是任何事情都有一個缺點:緩存中的數據可能不是最新的(如上例中,標誌字段的值未更新時)。所以volatile
關鍵字告訴 JVM 我們不想緩存我們的變量。這允許在所有線程上看到最新的結果。這是一個高度簡化的解釋。至於volatile
關鍵字,我強烈建議您閱讀這篇文章。有關更多信息,我還建議您閱讀Java 內存模型和Java Volatile 關鍵字。此外,重要的是要記住這volatile
是關於可見性,而不是關於更改的原子性。查看“競爭條件”部分中的代碼,我們將在 IntelliJ IDEA 中看到一個工具提示: 此檢查已作為IDEA-61117
原子性
原子操作是不可分割的操作。例如,給變量賦值的操作必須是原子的。不幸的是,遞增操作不是原子的,因為遞增需要多達三個 CPU 操作:獲取舊值,對其加一,然後保存該值。為什麼原子性很重要?隨著增量操作,如果出現競爭條件,那麼共享資源(即共享值)可能隨時突然改變。此外,涉及 64 位結構的操作(例如long
和double
)不是原子操作。可以在此處閱讀更多詳細信息:讀寫 64 位值時確保原子性。與原子性相關的問題可以在這個例子中看到:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
特殊AtomicInteger
班總是給我們30000,但是value
會時不時的變化。本主題有一個簡短的概述:Introduction to Atomic Variables in Java。“比較和交換”算法是原子類的核心。您可以在無鎖算法的比較 - JDK 7 和 8 示例中的 CAS 和 FAA或維基百科上的比較和交換文章中閱讀更多相關信息。 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
GO TO FULL VERSION