在高併發的狀況下,你的程序是否是常常出現一些詭異的BUG
,每次都是花費大量時間排查,可是你有沒有思考過這一切罪惡的源頭是什麼呢?java
CPU
、內存
、I/O設備
的速度差別愈來愈大,這也是程序性能的瓶頸,根據木桶理論,最終決定程序的總體性能取決於最慢的操做-讀寫I/O設備
,單方面的提升CPU的性能是無用的。面試
爲了平衡三者的差距,大牛前輩們不斷努力,最終作出了卓越的貢獻:編程
CPU
增長了緩存,平衡與內存之間的速度差別CPU
,進而均衡 CPU
與 I/O
設備的速度差別;注意:正是硬件前輩們作的這些貢獻,額外的後果須要軟件工程師來承擔,太坑了。緩存
在單核CPU的時代,全部的線程都在單個CPU上執行,不存在CPU數據和內存的數據的一致性。markdown
一個線程對共享變量的修改,另一個線程可以馬上看到,咱們稱爲可見性。併發
由於全部的線程都是在同一個CPU緩存中讀寫數據,一個線程對緩存的寫,對於另一個線程確定是可見的。以下圖:編程語言
從上圖能夠很清楚的瞭解,線程A對於變量的修改都是在同一個CPU緩存中,則線程B確定是可見的。高併發
可是多核時代的到來則意味着每一個CPU上都有一個獨立的緩存,信息再也不互通了,此時保證內存和CPU緩存的一致性就很難了。以下圖:性能
從上圖能夠很清楚的瞭解,線程A和線程B對變量A的改變是不可見的,由於是在兩個不一樣的CPU緩存中。優化
最簡單的證實方式則是在多核CPU的電腦上跑一個循環相加的方法,同時開啓兩個線程運行,最終獲得的結果確定不是正確的,以下:
public class TestThread {
private Long total=0L;
//循環一萬次相加
private void add(){
for (int i = 0; i < 10000; i++) {
total+=1;
}
}
//開啓兩個線程相加
public static void calc() throws InterruptedException {
TestThread thread=new TestThread();
//建立兩個線程
Thread thread1=new Thread(thread::add);
Thread thread2=new Thread(thread::add);
//啓動線程
thread1.start();
thread2.start();
//阻塞主線程
thread1.join();
thread2.join();
System.out.println(thread.total);
}
複製代碼
上述代碼在單核CPU的電腦上運行的結果確定是20000,可是在多核CPU的電腦上運行的結果則是在10000~20000
之間,爲何呢?
緣由很簡單,第一次在兩個線程啓動後,會將total=0
讀取到各自的CPU緩存中,執行total+1=0
後,各自將獲得的結果total=1
寫入到內存中(理想中應該是total=2
),因爲各自的CPU緩存中都有了值,所以每一個線程都是基於各自CPU緩存中的值來計算,所以最終致使了寫入內存中的值是在10000~20000
之間。
注意:若是循環的次數不多,這種狀況不是很明顯,若是次數設置的越大,則結果越明顯,由於兩個線程不是同時啓動的。
早期的操做系統是基於進程調度CPU,不一樣進程間是共享內存空間的,好比你在IDEA寫代碼的同時,可以打開QQ音樂,這個就是多進程。
操做系統容許某個進程執行一段時間,好比40毫秒,過了這個時間則會選擇另一個進程,這個過程稱之爲任務切換
,這個40毫秒稱之爲時間片
,以下圖:
在一個時間片內,若是一個進程進行IO操做,好比讀文件,這個時候該進程能夠把本身標記爲休眠狀態
並讓出CPU的使用權,待文件讀進內存,操做系統會將這個休眠的進程喚醒,喚醒後的進程就有機會從新得到CPU的使用權。
現代的操做系統更加輕量級了,都是基於線程調度,如今提到的任務切換
大都指示線程切換
。
注意:操做系統進行任務切換是基於
CPU指令
。
基於CPU指令是什麼意思呢?Java做爲高級編程語言,一條簡單的語句可能底層就須要多條CPU指令,例如total+=1
這條語句,至少須要三條CPU指令,以下:
total
從內存讀到CPU寄存器中基於CPU指令是什麼意思呢?簡單的說就是任務切換的時機多是上面的任何一條指令完成以後。
咱們假設在線程A執行了指令1後作了任務切換,此時線程B執行,雖然執行了total+1=1
,可是最終的結果卻不是2,以下圖:
咱們把一個或者多個操做在CPU執行過程當中不被中斷的特性稱之爲原子性。
注意:CPU僅僅能保證CPU指令執行的原子性,並不能保證高級語言的單條語句的原子性。
此處分享一道經典的面試題:
Long
類型的數據在32位操做系統中加減是否存在併發問題?答案:是,由於Long
類型是64位,在32位的操做系統中執行加減確定是要拆分紅多個CPU指令,所以沒法保證加減的原子性。
編譯優化算是最詭異的一個難題了,雖然高級語言規定了代碼的執行順序,可是編譯器有時爲了優化性能,則會改變代碼執行的順序,好比a=4;b=3;
,在代碼中可能給人直觀的感覺是a=4
先執行,b=3
後執行,可是編譯器可能爲了優化性能,先執行了b=3
,這種對於咱們肉眼是不可見的,上面例子中雖然不影響結果,可是有時候編譯器的優化可能致使意想不到的BUG。
雙重校驗鎖實現單例不知你們有沒有據說過,代碼以下:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
複製代碼
這裏我去掉了volatile
關鍵字,那麼此時這個代碼在併發的狀況下有問題嗎?
上述代碼看上去很完美,可是最大的問題就在new Singleton();
這行代碼上,預期中的new
操做順序以下:
Singleton
對象instance
變量可是實際上編譯優化後的執行順序以下:
instance
變量Singleton
對象不少人問了,優化後影響了什麼?
將內存N的地址提早賦值給instance
變量意味着instance!=null
是成立的,一旦是高併發的狀況下,線程A
執行第二步發生了任務切換
,則線程B
執行到了 if (instance == null)
這個判斷,此時不成立,則直接返回了instance
,可是此時的instance
並無初始化
過,若是此時訪問其中的成員變量則會發生空指針異常
,執行流程以下圖:
併發編程是區分高低手的門檻,只有深入理解三大特性:可見性
、原子性
、有序性
才能解決詭異的BUG
。
本文分析了帶來這三大特性源頭,以下:
可見性
問題原子性
問題有序性
問題另外,做者已經完成了兩個專欄的文章Mybatis進階、Spring Boot 進階 ,已經將專欄文章整理成書,有須要的公衆號回覆關鍵詞Mybatis 進階
、Spring Boot 進階
免費獲取。