引言
早期計算機中還不存在操作系統,一臺機器從頭到尾只能執行一個程序,並且這個程序能訪問所有的計算機資源。
操作系統的引入是的計算機“同時”能運行多個程序,不同程序都在單獨的進程中運行:操作系統爲各個獨立的進程分配各種資源,包括內存、文件句柄以及安全證書等。不同進程之間可以通過一些粗粒度的通信機制來交換數據,包括:套接字、信號處理器、共享內存、信號量以及文件等。
促成操作系統協調多進程同時運作的主要原因有:
- 資源利用率:對於某些需要等待IO或磁盤操作的程序,不應該要求CPU等待,在這種情況下,多進程操作系統的CPU可以在等待時運行其他程序;
- 公平性:通過時間分片使得不同程序對計算機的資源有同等的使用權;
- 便利性:一個系統的多個任務應該被拆分成多個進程進行獨立開發和維護,進程之間通過通信進行協調和共享數據;
而同樣的原因也促使線程的出現。線程也被稱爲輕量級進程,在大多數現代操作系統中,都是以線程爲基本的調度單位,而不是進程。
線程的出現,允許同一個進程中同時存在多個程序控制流,線程會共享進程範圍內的資源(正是因爲多線程共享同一個進程的資源,加大了多線程使用的負責性),例如內存句柄和文件句柄,每個線程都有各自的程序計數器、棧以及局部變量等。
多線程的優勢:
- 發揮多核處理器的強大能力;
- 建模的簡單性;
- 異步事件的簡化處理;
- 響應更靈敏的用戶界面;
多線程帶來的風險:
- 安全性問題:由於CPU對多個線程的調度存在隨機性,即在沒有充分運用同步機制(例如,volatile、synchronized、基於AQS的各種鎖)的情況下,多個線程的操作執行次序是不可預測的。
// java源碼
private int i; // 類字段
public void incAssign(){
i++;
}
// javap.exe -verbose查看class指令碼
public void incAssign();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0 // 將第一引用類型本地變量推送至棧頂
1: dup // 複製棧頂數值並將複製值壓入棧頂
2: getfield #2 // Field i:I // 獲取指定類的實例域,並將其值壓入棧頂
5: iconst_1 // 將int型1推送至棧頂
6: iadd // 將棧頂兩個int型值相加,並將結果壓入棧頂
7: putfield #2 // Field i:I // 爲指定類的實例域賦值
10: return // 從當前方法返回void
LineNumberTable:
line 10: 0
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LTest;
}
分析上述源碼和編譯後的字節碼可知,value++看上去是單個操作,但事實上在JVM執行字節碼時,它包含了幾個獨立的指令操作,如本例中的2-7指令行:獲取實例域數值並壓入棧頂、將常量1推送至棧頂、棧頂的兩個值相加、給實例域重新賦值,即一個簡單的自加操作,在字節碼的處理過程中至少涉及到了四步驟的操作。
如果A線程執行到第6行,還尚未執行第7行的賦值,而此時B線程已經執行完第2行的讀取指令,那麼A線程的加1的效果還沒來得及被B線程讀取到,也就意味着兩個線程對同一個i的值進行自加操作,雖然加了2次,但兩者得到的結果是一樣的(B在A之後putfield,相當於把A的勞動成果覆蓋掉了)。這就是所謂的多線程併發執行時的不安全問題,而本例還尚未考慮指令重排序帶來的更多複雜性。