Java的併發程式設計中的多執行緒問題到底是怎麼回事兒?

HollisChuang發表於2018-07-25

在我之前的一篇《再有人問你Java記憶體模型是什麼,就把這篇文章發給他。》文章中,介紹了Java記憶體模型,通過這篇文章,大家應該都知道了Java記憶體模型的概念以及作用,這篇文章中談到,在Java併發程式設計中,通常會遇到三個問題,即原子性問題、一致性問題和有序性問題。

上面一篇文章簡單介紹了一下,由於各種原因會導致多執行緒場景下可能存在原子性、一致性和有序性問題。但是並沒有深入,這篇文章就來在之前的基礎上,再來看一下,併發程式設計中,這些問題都是哪來的?

首先,我們還是從作業系統開始,先來了解一些基礎知識。

CPU時間片

很多人都知道,現在我們用到作業系統,無論是Windows、Linux還是MacOS等其實都是多使用者多工分時作業系統。使用這些作業系統的“使用者”是可以“同時”幹多件事的,這已經是日常習慣了,並沒覺得有什麼特別。

但是實際上,對於單CPU的計算機來說,在CPU中,同一時間是隻能幹一件事兒的。

為了看起來像是“同時幹多件事”,分時作業系統是把CPU的時間劃分成長短基本相同的時間區間,即”時間片”,通過作業系統的管理,把這些時間片依次輪流地分配給各個“使用者”使用。

如果某個“使用者”在時間片結束之前,整個任務還沒有完成,“使用者”就必須進入到就緒狀態,放棄CPU,等待下一輪迴圈。此時CPU又分配給另一個“使用者”去使用。

CPU 就好像是一個電話亭,他可以開放給所有使用者使用,但是他有規定,每個使用者進入電話亭之後只能使用規定時長的時間。如果時間到了,使用者還沒打完電話,那就會被要求去重新排隊。

不同的作業系統,在選擇“使用者”分配時間片的排程演算法是不一樣的,常用的有FCFS、輪轉、SPN、SRT、HRRN、反饋等,由於不是本文重點,就不展開了。

這個電話亭可以允許哪個使用者進入打電話是有不同的策略的,不同的電話亭規定不同,有的電話亭採用排隊機制(FCFS)、有的優先分配給打電話時間最短的人(SPN)等

程式與執行緒

前面介紹CPU時間片的時候提到了CPU會根據不同的排程演算法把時間片分配給“使用者”,這裡的“使用者”在以前指的是程式,隨著作業系統的不斷髮展,現在一般指執行緒。

在過去沒有執行緒的作業系統中,資源的分配和執行都是由程式完成的。隨著技術的發展,為了減少由於程式切換帶來的開銷,提升併發能力,作業系統中引入執行緒。把原本屬於程式的工作一分為二,程式還是負責資源的分配,而執行緒負責執行。

也就是說,程式是資源分配的基本單位,而執行緒是排程的基本單位。

多執行緒中的併發問題

瞭解了以上的和硬體及作業系統有關的基礎知識以後,我們再來看下,在多執行緒場景中有哪些併發問題。

關於併發程式設計中的原子性、可見性和有序性問題我在《記憶體模型》一文介紹過。

文中提到:快取一致性問題其實就是可見性問題。而處理器優化是可以導致原子性問題的。指令重排即會導致有序性問題。有部分讀者對這部分不是很理解。由於上一篇文章主要介紹記憶體模型,並沒有展開分析,只是給了個結論,這裡再針對這部分深入分析一下。

由於快取一致性問題導致可見性問題,在《記憶體模型》中介紹的很清晰了,這裡就不贅述了,主要結合本文來分析下原子性問題和有序性問題。

原子性問題

我們說原子性問題,其實指的是多執行緒場景中操作如果不能保證原子性,會導致處理結果和預期不一致。

前面我們提到過,執行緒是CPU排程的基本單位。CPU有時間片的概念,會根據不同的排程演算法進行執行緒排程。所以在多執行緒場景下,就會發生原子性問題。因為執行緒在執行一個讀改寫操作時,在執行完讀改之後,時間片耗完,就會被要求放棄CPU,並等待重新排程。這種情況下,讀改寫就不是一個原子操作。

就好像我們去電話亭打電話,一共有三個步驟,查詢電話,撥號,交流。由於我們在電話亭中可以停留的時間有限,有可能剛剛找到電話號碼,時間到了,就被趕出來了。

在單執行緒中,一個讀改寫就算不是原子操作也沒關係,因為只要這個執行緒再次被排程,這個操作總是可以執行完的。但是在多執行緒場景中可能就有問題了。因為多個執行緒可能會對同一個共享資源進行操作。

比如經典的 i++ 操作,對於一個簡單的i++操作,一共有三個步驟:load , add ,save 。共享變數就會被多個執行緒同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變數的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。

111

有序性問題

而且,我們知道,除了引入了時間片以外,由於處理器優化和指令重排等,CPU還可能對輸入程式碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是有序性問題。

我們打電話的時候,除了可能被中途趕出來以外,本來正常步驟是要查詢電話、撥號、交流的。但是電話亭非要給我們優化成查詢電話、交流、撥號。這肯定不是我們想要的啊。

還是剛剛的i++操作,在滿足了原子性的情況下,如果沒有滿足有序性,那麼得到的結果可能也不是我們想要的。

222

總結

本文主要介紹了併發程式設計中會導致原子性和有序性問題的原因,關於可見性請參考《記憶體模型》。關於這三種問題的解決方案在《記憶體模型》也有介紹,更多的可以參考多執行緒相關書籍。Hollis後續也會出更多文章再深入分析,敬請期待。

wechat

參考資料

再有人問你Java記憶體模型是什麼,就把這篇文章發給他。 伺服器效能指標(二)——CPU利用率分析及問題排查 聊聊併發(五)——原子操作的實現原理

相關文章