並發編程中的三個關鍵字【原子性、可見性、有序性】

  • 2019 年 10 月 8 日
  • 筆記

並發問題是絕大部分的程序員頭疼的問題,但仍需面對。因此,將需要的原理進行拆分解讀,會讓其變得易讀一些。共勉~ —— 23號老闆

0

1

高速緩存

在計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU裏面就有了高速緩存。

也就是,當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。

例如 i = i + 1;

當線程執行這個語句時,會先從主存當中讀取i的值,然後複製一份到高速緩存當中,然後CPU執行指令對i進行加1操作,然後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。

這個代碼在單線程中運行是沒有任何問題的,但是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行於不同的CPU中,因此每個線程運行時有自己的高速緩存(對單核CPU來說,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的)。本文我們以多核CPU為例。

比如同時有2個線程執行這段代碼,假如初始時i的值為0,那麼我們希望兩個線程執行完之後i的值變為2。但是事實會是這樣嗎?

可能存在下面一種情況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,然後線程1進行加1操作,然後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值還是0,進行加1操作之後,i的值為1,然後線程2把i的值寫入內存。

最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。通常稱這種被多個線程訪問的變量為共享變量。

0

2

緩存一致性問題

也就是說,如果一個變量在多個CPU中都存在緩存(一般在多線程編程時才會出現),那麼就可能存在緩存不一致的問題。

為了解決緩存不一致性問題,通常來說有以下2種解決方法:

 1)通過在總線加LOCK#鎖的方式

 2)通過緩存一致性協議

這2種方式都是硬件層面上提供的方式。

在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。因為CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。比如上面例子中 如果一個線程在執行 i = i +1,如果在執行這段代碼的過程中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼完全執行完畢之後,其他CPU才能從變量i所在的內存讀取變量,然後進行相應的操作。這樣就解決了緩存不一致的問題。

但是上面的方式會有一個問題,由於在鎖住總線期間,其他CPU無法訪問內存,導致效率低下。

所以就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

在並發編程中,我們通常會遇到三個常見的關鍵字:

1、原子性

即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

2、可見性

指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

3、一致性。

即程序執行的順序按照代碼的先後順序執行。

* 對於一致性的問題,一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。這就涉及到所謂的指令重排序問題。

處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。

指令重排序不會影響單個線程的執行,但是會影響到線程並發執行的正確性。

也就是說,要想並發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。

0

3

Java內存模型

在Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽各個硬件平台和操作系統的內存訪問差異,以實現讓Java程序在各種平台下都能達到一致的內存訪問效果。

Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。

舉個簡單的例子:在java中,執行下面這個語句:

i = 10;

執行線程必須先在自己的工作線程中對變量i所在的緩存行進行賦值操作,然後再寫入主存當中。而不是直接將數值10寫入主存當中。

那麼Java語言本身對原子性、可見性以及有序性提供了哪些保證呢?

0

4

小結

今天時間不多,還請大家見諒~

下一篇繼續。未完待續……

文章內容參考至Matrix海子,在此鳴謝。

https://www.cnblogs.com/dolphin0520/p/3920373.html