1. 程式人生 > >Java的並發編程中的多線程問題到底是怎麽回事兒?

Java的並發編程中的多線程問題到底是怎麽回事兒?

linu 微信 cpu調度 是我 依次 中介 更多 能力 是什麽

在我之前的一篇《再有人問你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++操作,在滿足了原子性的情況下,如果沒有滿足有序性,那麽得到的結果可能也不是我們想要的。

總結

本文主要介紹了並發編程中會導致原子性和有序性問題的原因,關於可見性請參考《內存模型》。關於這三種問題的解決方案在《內存模型》也有介紹,更多的可以參考多線程相關書籍。後續也會出更多文章再深入分析,敬請期待。 微信公眾號搜索Hollis即可!

Java的並發編程中的多線程問題到底是怎麽回事兒?