1. 線程安全問題
1.1 什麼是線程安全?
- 某權威作者定義:“當多個線程訪問同一個對象時,如果不用考慮這些線程的調度,也不需要額外的同步,調用這個對象的行爲都可以獲得正確的結果,那麼這個對象時線程安全的“。
- 通俗易懂的定義:當多個線程訪問同一個對象時,不需要做額外的任何處理,像單線程編程一樣,程序可以正常運行,就可以稱爲線程安全。
1.2 線程安全問題有哪些?
- 運行結果錯誤:a++多線程下出現結果錯誤
- 活躍性問題:死鎖、活鎖、飢餓
1.2.1 運行結果錯誤
下面演示a++運行結果錯誤:
public class APlusPlus implements Runnable {
static int a = 0;
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
a++;
}
}
public static void main(String[] args) throws InterruptedException {
APlusPlus aPlusPlus = new APlusPlus();
Thread t1 = new Thread(aPlusPlus);
Thread t2 = new Thread(aPlusPlus);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
}
11054
我們預計的結果是20000,最後結果比預計的少,因爲a++,看上去是一個操作,實際上包含了三個動作:
1. 讀取a
2. 將a加一
3. 將a的值寫入到內存中
說明a++操作是不具備原子性的,因此需要對a++操作進行同步,保證同一時刻只有一個線程執行a++操作。
1.2.2 活躍性問題:死鎖、活鎖、飢餓
下面演示死鎖問題:
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "獲取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在嘗試獲取lockB...");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "獲取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "獲取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在嘗試獲取lockA...");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "獲取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
t2.start();
}
}
由於t1獲取到lockA,t2獲取到lockB,t1和t2分別想獲取lockB和lockA,由於雙方都沒有釋放鎖,所以t1和t2都進入到了死鎖狀態。
1.3 各種需要考慮線程安全的情況
- 訪問共享變量,比如對象的屬性、靜態變量、共享緩存等;
- 所有依賴順序的操作,比如:讀改寫(原子性)、讀取-檢查-操作(內存可見性);
- 數據之間存在綁定關係;
- 使用其他類的時候,比如使用HashMap就不是線程安全的;
2. 性能問題
爲什麼多線程會帶來性能問題?一共分爲以下兩個原因:
- 上下文切換
- 內存同步
2.1 上下文切換
- 什麼是上下文切換:進行一次上下文切換時,先掛起當前線程,然後把當前線程狀態存在某處,以便線程切換回來知道執行到哪裏了,這個線程狀態包含當前線程執行的指令和位置,這個狀態就是上下文。
- 由於上下文切換也會使得CPU自身的一些緩存失效,相對而言,也降低了執行速度。
- 何時會密集地導致上下文切換:搶鎖、IO
2.2 內存同步
- 由於JMM規定了主內存和線程內存,而線程內存會緩存數據,可以增加計算速度。由於在多線程編程時使用了synchronized、volatile關鍵字等,禁止了線程緩存,也禁止了指令重排序,編譯器CPU等優化手段,使得是能只用主內存了,相對而言就降低了性能