前言:本系列致力于理清并發編程的理論(其一)和使用Java程序實現并發編程(其二)
多線程的優勢
- 發揮多處理器的優勢
- 建模更加簡單
第一點很好理解:處理器的調度單位是線程,如果程序只有一個線程,最多只能同時在一臺處理器上運行,不能發揮多處理器的效率。
第二點,簡單點說,就是分工協作。例如一個程序有幾個不同的任務可以并發執行,那么在實現過程中,就可以根據任務的不同,在不同的線程中來完成不同的任務。例如:JVM中的垃圾收集器。
其實多線程的優勢有很多,知識相對理論化,我們在最開始提到這些知識點,理由很簡單:『如果要做一件事,總得知道為什么要做吧!』
多線程帶來的風險
- 安全性問題
- 活躍性問題
- 性能問題
線程安全性
概念上來說:如果多個線程訪問一個類,不管處理器采用何種調度順序執行,這個類都能表現出正確的行為——那么這個類就是『線程安全』的。
簡言之:一個類的行為和其規范完全一致。
//計數器程序
public int count(){
return value++;
}
value++實際包含三個操作步驟:
read value(假設為1) --> 1+1=2 --> write 2 to value
所以很容易發生這種情況:
Time | T1 | T2 | T3 | T4 |
---|---|---|---|---|
線程A | read value 9 | 9+1=10 | write value with 10 | |
線程B | read value 9 | 9+1=10 | write value with 10 |
雖然有兩個線程調用了計數器程序,但是因為處理器的交替執行,導致最終的結果與期望結果不符合,所以這個方法『不是線程安全』的。
活躍性問題
例如:死鎖、饑餓、活鎖。
性能問題
多線程程序中,線程調度器臨時掛起活躍線程,轉而執行執行另外一個線程時,需要執行『上下文切換』操作,這些操作也會占用系統資源。所以,只有在設計良好的并發應用程序中,多線程才能提高程序的效率。
共享狀態和原子操作
共享狀態很好理解,例如前面的例子中的count方法。如果線程A和線程B同時都調用了同一個對象object的count方法,那么,這兩個線程都可以同時訪問對象object的value域,對象就處于共享狀態。
原子操作。前面提到的count++我們已經提到了,實際處理過程中包含三個步驟,所以并不是原子操作。
綜合以上兩點:一個對象處在共享狀態中,并且對于對象的共享狀態變量修改操作不是原子操作,這樣一個對象,肯定不是線程安全的。那么想要保證對象的線程安全性,只能確保對于對象的修改操作是原子性的。
原子變量類 & 加鎖機制
原子變量類估計很少有人聽說,因為這種方法很少使用,這里簡單介紹:例如,可以使用AtomicLong代替Long或者int類型的count,用于計數。
//聲明為final可確保不被在程序無意修改
private final AtomicLong count = new AtomicLong(0);
count.incrementAndGet();//原子操作
加鎖機制就是指我們常用的Java內置鎖:同步代碼塊(Synchronized block)機制。一個同步代碼塊可以看成一個原子操作。
Java中的同步代碼塊實際包括兩個部分:一是作為『鎖』的對象引用,也就是共享的可變變量;二是作為由這個鎖保護的同步代碼塊。
Synchronized(object) { //object作為鎖的對象引用
//同步代碼塊
}
我們經常能看到以關鍵字Synchronized來修飾的方法,其實際上就是一個橫跨整個方法體的同步代碼塊。
重入:一個線程可以獲得一個已經由它自己持有的鎖。Java內置鎖為可重入的鎖。
未完待續。。。