計算機發(fā)展過程中存在一個核心矛盾:CPU、內(nèi)存與I/O設(shè)備,這三者的速度差異。
形象比喻:
1.CPU是天上一天,內(nèi)存地上一年(假設(shè)CPU執(zhí)行普通指令需要一天,那么CPU讀寫內(nèi)存需要一年)。
2.內(nèi)存是天上一天,I/O是地上十年。
總結(jié):根據(jù)木桶原理(一只桶能裝多少水取決于最短的那塊木板),
程序整體性能取決于最慢的I/O設(shè)備。單方面提升CPU性能是無效的。
(類比機械硬盤與固態(tài)硬盤對性能的提升。只更換為機械硬盤,內(nèi)存和CPU不變,你的電腦都能有很大改善。)
如何平衡速度差異:
1.CPU加緩存----------平衡與內(nèi)存速度差異。
舉例:(數(shù)組在內(nèi)存中是占據(jù)連續(xù)的內(nèi)存空間的,而CPU在從內(nèi)存中讀取數(shù)據(jù)的時候會把該內(nèi)存地址后面的一部分數(shù)據(jù)也
緩存進去。這樣CPU在訪問數(shù)組數(shù)據(jù)的時候先從CPU緩存的數(shù)組中尋找,找不到再從內(nèi)存中復(fù)制。這也就是CPU緩存的意義,)
2.操作系統(tǒng)增加了進程、線程以分時復(fù)用CPU-------平衡CPU和I/O設(shè)備的速度差異。
3.編譯程序優(yōu)化指令執(zhí)行次序,使得緩存利用更加合理。
緩存導(dǎo)致的可見性問題
在如今的多核時代,每科CPU都有自己的緩存,當多個線程在不同CPU上執(zhí)行,這些線程操作的是不同的緩存。某個線程對共享變量的操作,會出現(xiàn)對另外線程不可見的情況。
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 創(chuàng)建兩個線程,執(zhí)行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
th1.start();
th2.start();
th1.join();
th2.join();
return count; //最終結(jié)果會是10000到200000之間的隨機數(shù)
}
}
線程切換帶來的原子性問題
時間片和任務(wù)切換簡單理解:
1.操作系統(tǒng)運行某個進程執(zhí)行一小段時間,假如50毫秒,過了50毫秒后操作系統(tǒng)會選擇新的進程執(zhí)行(稱之為“任務(wù)切換”)
,這50毫秒稱為時間片。

時間片.png
線程調(diào)度
計算機通常只有一個CPU,在任意時刻只能執(zhí)行一條機器指令,每個線程只有獲得CPU
的使用權(quán)才能執(zhí)行指令。所謂多線程的并發(fā)運行,其實是指從宏觀上看,各個線程輪流獲
得CPU的使用權(quán),分別執(zhí)行各自的任務(wù)。在運行池中,會有多個處于就緒狀態(tài)的線程在等
待CPU,JAVA[虛擬機]的一項任務(wù)就是負責線程的調(diào)度,線程調(diào)度是指按照特定機制為多
個線程分配CPU的使用權(quán)。
參考百度百科:https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E8%B0%83%E5%BA%A6/10226112
調(diào)度模型:分時調(diào)度模型和搶占式調(diào)度模型。

線程調(diào)度模型.png
放棄CPU使用權(quán)的原因:
1.java虛擬機讓當前線程暫時放棄CPU,轉(zhuǎn)到就緒狀態(tài),使其它線程獲得運行機會。
2.當前線程因為某些原因而進入阻塞狀態(tài)。
3. 線程結(jié)束運行。

放棄cpu的原因.png
bug源之一:任務(wù)切換------非原子性。(操作系統(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU指令執(zhí)行完,而不是高級語言的一條指令。)
以代碼 count+=1為例子。
指令1:把變量count加載到CPU寄存器。
指令2:在寄存器執(zhí)行+1操作。
指令3:將結(jié)果寫入內(nèi)存。(緩存機制可能導(dǎo)致寫入的是CPU緩存。)
圖示:當兩個線程由于任務(wù)切換,出現(xiàn)這種情況。線程B沒有在線程A的結(jié)果基礎(chǔ)上進行操作。也就是說線程A count+=1不具備原子性。會導(dǎo)結(jié)果異常。

線程切換.png
編譯器優(yōu)化:有序性問題
編譯器為了優(yōu)化性能,有時候會改變程序語句執(zhí)行順序。例如 a = 6; b = 7這樣的順序可能有化成b=7;a=6;大多時候不影響程序最終結(jié)果。不過有時候編譯器及解釋器的優(yōu)化會導(dǎo)致意想不到的BUG。
以java中經(jīng)典的一個單例模式寫法為例。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){//獲取單例
if (instance == null) { //首先判斷是否為空
synchronized(Singleton.class) {//為空就加鎖
if (instance == null)//并再次檢查instance是否為空
instance = new Singleton();//如果空就創(chuàng)建實例
}
}
return instance;
}
}
解釋雙重檢查目的,避免每次都進行加鎖操作。
java中的new操作
1.分配一塊內(nèi)存M。
2.在內(nèi)存M上初始化Singleton對象。
3.將M的地址賦值給instance對象。
優(yōu)化后會變成這樣:
1.分配一塊內(nèi)存M。
2.將M的地址賦值給instance對象。
3.在內(nèi)存M上初始化Singleton對象。
上面的new操作在多線程中會出現(xiàn)這樣的情況。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) { //線程B剛執(zhí)行這,發(fā)現(xiàn)不為空,立即返回,而線程A由于優(yōu)化了new的執(zhí)行順序還沒有真正
//的初始化。這時會導(dǎo)致空指針異常。
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton(); }
}
return instance;
}
}
并發(fā)涉及的知識面挺廣的,推薦閱讀:http://gk.link/a/103WI