因爲IO太慢,早期的操做系統就發明了多進程,即使在單核的CPU上咱們也能夠一邊聽着歌,一邊寫Bug,這個就是多進程的功勞。操做系統容許某個進程執行一小段時間,例如50毫秒,過了50毫秒操做系統就會從新選擇一個進程來執行(咱們稱爲「任務切換」),這個50毫秒稱爲「時間片」。程序員
在一個時間片內,若是一個進程進行一個IO操做,例如讀個文件,這個時候該進程能夠把本身標記爲「休眠狀態」並出讓CPU的使用權,待文件讀進內存,操做系統會把這個休眠的進程喚醒,喚醒後的進程就有機會從新得到CPU的使用權了。這裏的進程在等待IO時之因此會釋放CPU使用權,是爲了讓CPU在這段等待時間裏能夠作別的事情,這樣一來CPU的使用率就上來了;此外,若是這時有另一個進程也讀文件,讀文件的操做就會排隊,磁盤驅動在完成一個進程的讀操做後,發現有排隊的任務,就會當即啓動下一個讀操做,這樣IO的使用率也上來了。早期的操做系統基於進程來調度CPU,不一樣進程間是不共享內存空間的,因此進程要作任務切換就要切換內存映射地址,而一個進程建立的全部線程,都是共享一個內存空間的,因此線程作任務切換成本就很低了。現代的操做系統都基於更輕量的線程來調度,如今咱們提到的「任務切換」都是指「線程切換」。Java併發程序都是基於多線程的,天然也會涉及到任務切換,也許你想不到,任務切換居然也是併發編程裏詭異Bug的源頭之一。任務切換的時機大多數是在時間片結束的時候,咱們如今基本都使用高級語言編程,高級語言裏一條語句每每須要多條CPU指令完成,例如count += 1,至少須要三條CPU指令。指令1:首先,須要把變量count從內存加載到CPU的寄存器;指令2:以後,在寄存器中執行+1操做;指令3:最後,將結果寫入內存(緩存機制致使可能寫入的是CPU緩存而不是內存)。操做系統作任務切換,能夠發生在任何一條CPU指令執行完,是的,是CPU指令,而不是高級語言裏的一條語句。對於上面的三條指令來講,咱們假設count=0,若是線程A在指令1執行完後作線程切換,線程A和線程B按照下圖的序列執行,那麼咱們會發現兩個線程都執行了count+=1的操做,可是獲得的結果不是咱們指望的2,而是1。面試
非原子操做的執行路徑示意圖咱們潛意識裏面以爲count+=1這個操做是一個不可分割的總體,就像一個原子同樣,線程的切換能夠發生在count+=1以前,也能夠發生在count+=1以後,但就是不會發生在中間。咱們把一個或者多個操做在CPU執行的過程當中不被中斷的特性稱爲原子性。CPU能保證的原子操做是CPU指令級別的,而不是高級語言的操做符,這是違背咱們直覺的地方。所以,不少時候咱們須要在高級語言層面保證操做的原子性。編程
========併發
若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:ide
點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。高併發
關注公衆號 『 Java鬥帝 』,不按期分享原創知識。職業規劃
同時能夠期待後續文章ing?spa