1.併發編程領域可以抽象成三個核心問題:分工、同步和互斥
分工指的是如何高效地拆解任務並分配給線程,而同步指的是線程之間如何協作,互斥則是保證同一時刻只允許一個線程訪問共享資源。
2.併發編程領域問題的產生原因:
源頭之一:緩存導致的可見性問題
一個線程對共享變量的修改,另外一個線程能夠立刻看到,我們稱爲可見性。
源頭之二:線程切換帶來的原子性問題
我們把一個或者多個操作在CPU執行的過程中不被中斷的特性稱爲原子性。
源頭之三:編譯優化帶來的有序性問題
編譯器爲了優化性能,有時候會改變程序中語句的先後順序,編譯器調整了語句的順序,大多數情況下不影響程序的最終結果。不過有時候編譯器及解釋器的優化可能導致意想不到的 Bug。
緩存導致的可見性問題,線程切換帶來的原子性問題,編譯優化帶來的有序性問題,其實緩存、線程、編譯優化的目的和我們寫併發程序的目的是相同的,都是提高程序性能。但是技術在解決一個問題的同時,必然會帶來另外一個問題,所以在採用一項技術的同時,一定要
清楚它帶來的問題是什麼,以及如何規避。
解決可見性、有序性合理的方案應該是按需禁用緩存以及編譯優化。解決原子性問題需要互斥鎖。
3.死鎖
死鎖的定義是:一組互相競爭資源的線程因互相等待,導致“永久”阻塞的現象。
只有以下這四個條件都發生時纔會出現死鎖:
互斥,共享資源 X 和 Y 只能被一個線程佔用;
佔有且等待,線程 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
不可搶佔,其他線程不能強行搶佔線程 T1 佔有的資源;
循環等待,線程 T1 等待線程 T2 佔有的資源,線程 T2 等待線程 T1 佔有的資源,就是循環等待。
反過來分析,也就是說只要我們破壞其中一個,就可以成功避免死鎖的發生。其中,互斥這個條件我們沒有辦法破壞,因爲我們用鎖爲的就是互斥。不過其他三個條件都是有辦法破壞掉的,到底如何做呢?對於“佔用且等待”這個條件,我們可以一次性申請所有的資源,這樣
就不存在等待了。對於“不可搶佔”這個條件,佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。對於“循環等待”這個條件,可以靠按序申請資源來預防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化後自然就不存在循環了。
4.等待-通知機制
synchronized用法:
class Allocator {
private List<Object> als;
// 一次性申請所有資源
synchronized void apply(bject from, Object to){
// 經典寫法
while(als.contains(from) ||als.contains(to)){
try{
wait();
}catch(Exception e){
}
}
als.add(from);
als.add(to);
}
// 歸還資源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}
關於notify() 和notifyAll():
notify() 是會隨機地通知等待隊列中的一個線程,而 notifyAll() 會通知等待隊列中的所有線程。從感覺上來講,應該是notify()更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但那所謂的感覺往往都蘊藏着風險,實際上使用notify()也很有風險,它的風險在於可能導致某些線程永遠不會被通知到。
除非經過深思熟慮,否則儘量使用notifyAll()。那什麼時候可以使用 notify() 呢?需要滿足以下三個條件:
1.所有等待線程擁有相同的等待條件;
2.所有等待線程被喚醒後,執行相同的操作;
3.只需要喚醒一個線程。
lock用法:
public class BlockedQueue<T>{
final Lock lock = new ReentrantLock();
// 條件變量:隊列不滿
final Condition notFull = lock.newCondition();
// 條件變量:隊列不空
final Condition notEmpty = lock.newCondition();
// 入隊
void enq(T x) {
lock.lock();
try {
while (隊列已滿){
// 等待隊列不滿
notFull.await();
}
// 省略入隊操作...
//入隊後,通知可出隊
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出隊
void deq(){
lock.lock();
try {
while (隊列已空){
// 等待隊列不空
notEmpty.await();
}
// 省略出隊操作...
//出隊後,通知可入隊
notFull.signal();
}finally {
lock.unlock();
}
}
}
5.創建多少線程合適?
對於 CPU 密集型的計算場景,在工程上,線程的數量一般會設置爲“CPU核數+1”,這樣的話,當線程因爲偶爾的內存頁失效或其他原因導致阻塞時,這個額外的線程可以頂上,從而保證 CPU 的利用率。
對於 I/O 密集型的計算場景,最佳的線程數是與程序中CPU計算和I/O操作的耗時比相關的,我們可以總結出這樣一個公式:
最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]