標籤:rect 根據 str 記憶體 data- 緩衝 分時 分析 win
在我之前的一篇《再有人問你Java記憶體模型是什麼,就把這篇文章發給他。》文章中,介紹了Java記憶體模型,通過這篇文章,大家應該都知道了Java記憶體模型的概念以及作用,這篇文章中談到,在Java並發編程中,通常會遇到三個問題,即原子性問題、一致性問題和有序性問題。
上面一篇文章簡單介紹了一下,由於各種原因會導致多線程情境下可能存在原子性、一致性和有序性問題。但是並沒有深入,這篇文章就來在之前的基礎上,再來看一下,並發編程中,這些問題都是哪來的?
首先,我們還是從作業系統開始,先來瞭解一些基礎知識。
CPU時間片
很多人都知道,現在我們用到作業系統,無論是Windows、Linux還是MacOS等其實都是多使用者多任務分時作業系統。使用這些作業系統的“使用者”是可以“同時”幹多件事的,這已經是日常習慣了,並沒覺得有什麼特別。
但是實際上,對於單CPU的電腦來說,在CPU中,同一時間是只能幹一件事兒的。
為了看起來像是“同時幹多件事”,分時作業系統是把CPU的時間劃分成長短基本相同的時間區間,即”時間片”,通過作業系統的管理,把這些時間片依次輪流地分配給各個“使用者”使用。
如果某個“使用者”在時間片結束之前,整個任務還沒有完成,“使用者”就必須進入到就緒狀態,放棄CPU,等待下一輪迴圈。此時CPU又分配給另一個“使用者”去使用。
不同的作業系統,在選擇“使用者”分配時間片的調度演算法是不一樣的,常用的有FCFS、輪轉、SPN、SRT、HRRN、反饋等,由於不是本文重點,就不展開了。
進程與線程
前面介紹CPU時間片的時候提到了CPU會根據不同的調度演算法把時間片分配給“使用者”,這裡的“使用者”在以前指的是進程,隨著作業系統的不斷髮展,現在一般指線程。
在過去沒有線程的作業系統中,資源的分配和執行都是由進程完成的。隨著技術的發展,為了減少由於進程切換帶來的開銷,提升並發能力,作業系統中引入線程。把原本屬於進程的工作一分為二,進程還是負責資源的分配,而線程負責執行。
也就是說,進程是資源分派的基本單位,而線程是調度的基本單位。
多線程中的並發問題
瞭解了以上的和硬體及作業系統有關的基礎知識以後,我們再來看下,在多線程情境中有哪些並發問題。
關於並發編程中的原子性、可見度和有序性問題我在《記憶體模型》一文介紹過。
文中提到:緩衝一致性問題其實就是可見度問題。而處理器最佳化是可以導致原子性問題的。指令重排即會導致有序性問題。有部分讀者對這部分不是很理解。由於上一篇文章主要介紹記憶體模型,並沒有展開分析,只是給了個結論,這裡再針對這部分深入分析一下。
由於緩衝一致性問題導致可見度問題,在《記憶體模型》中介紹的很清晰了,這裡就不贅述了,主要結合本文來分析下原子性問題和有序性問題。
原子性問題
我們說原子性問題,其實指的是多線程情境中操作如果不能保證原子性,會導致處理結果和預期不一致。
前面我們提到過,線程是CPU調度的基本單位。CPU有時間片的概念,會根據不同的調度演算法進行線程調度。所以在多線程情境下,就會發生原子性問題。因為線程在執行一個讀改寫操作時,在執行完讀改之後,時間片耗完,就會被要求放棄CPU,並等待重新調度。這種情況下,讀改寫就不是一個原子操作。
在單線程中,一個讀改寫就算不是原子操作也沒關係,因為只要這個線程再次被調度,這個操作總是可以執行完的。但是在多線程情境中可能就有問題了。因為多個線程可能會對同一個共用資源進行操作。
比如經典的 i++ 操作,對於一個簡單的i++操作,一共有三個步驟:load , add,save 。共用變數就會被多個線程同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共用變數的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。
有序性問題
而且,我們知道,除了引入了時間片以外,由於處理器最佳化和指令重排等,CPU還可能對輸入代碼進行亂序執行,比如load->add->save 有可能被最佳化成load->save->add 。這就是有序性問題。
還是剛剛的i++操作,在滿足了原子性的情況下,如果沒有滿足有序性,那麼得到的結果可能也不是我們想要的。
總結
本文主要介紹了並發編程中會導致原子性和有序性問題的原因,關於可見度請參考《記憶體模型》。關於這三種問題的解決方案在《記憶體模型》也有介紹,更多的可以參考多線程相關書籍。
Java的並發編程中的多線程問題到底是怎麼回事兒?