Java Concurrency #1: Concurrency 基礎

Charlie Lee
Bucketing
Published in
10 min readJun 30, 2020

在開始使用Thread前先瞭解Concurrency,更能體會Java Concurrency的能力

Photo by Yogesh Phuyal on Unsplash

項目

  • 什麼是Concurrency
  • 為什麼需要Concurrency
  • Concurrency造成的問題
  • Java Memory Model

什麼是Concurrency

Concurrency的中文翻譯是併發,直接看字面含意就是一起做一件事情,直接透過兩張圖與一個故事理解甚麼事Concurrency。

下圖,當一個應用程式在執行一個功能時可能這個功能會包含兩個任務,例如:從DB透過使用者下的關鍵字找到對應商品,與透過此關鍵字找到相對應常販售此關鍵字的賣家資訊,在一般情況下會很直覺得寫出類似的程式碼

// 虛擬碼
public Data searchData() {
SubData sd = searchPurchase();
SubData sd2 = searchSameTypePurchaseSaler();
return new Data(sd, sd2);
}

這樣的執行效率會如下圖,

因為搜尋DB需要一定的時間,如果單純用一個執行緒執行那上面那段程式碼需要的時間會是2n Sec,但是其實這兩個搜尋任務只要有商品的Idx就可以個別搜尋,不需要在同個執行緒跑,將任務接分成三個執行緒執行(主執行緒-責任:啟動子任務 / 子執行緒 *2 分別去跑搜尋商品資訊/相關商品賣家),那執行時間就回如下圖

而兩個Thread互相交互的行為,也稱為Concurrency,在以前的電腦架構,Concurrency只是概念上的發生,CPU快速切換Thread讓使用者有種同步進行的感覺,但現在的架構Concurrency已經變成真實物理的發生,因為電腦有不止一個CPU。

Concurrency如何來的

首先要先知道計算機組織中每個元件的速度差異(Base on 2012)

  • CPU Cache — L1(0.5ns) L2(7ns)
  • Memory — 100ns
  • I/O device — network (10us / 1k over 1G) / Disk seek (10ms)

在最原始的計算機架構中,三者速度的差異讓CPU為了等待從Memory或I/O拿資料而有空閒的時間,為了讓CPU可以最大化利用(在等待I/O或Memory讀取的同時去做其他事情),計算機架構進而演進了現在的樣子

  1. 增加CPU Cache - 讓資料可以先暫存於CPU中,不需對Memory做I/O
  2. 操作系統的資源切割Procss和時間切割Thread概念,讓CPU可以在人類感受不到的時間內做資源和時間的切換(Context Switch)
  3. 編譯語言的出現,將開發者的設計邏輯,進一步調整成可以讓CPU效率最大化的順序

而第二點,將CPU使用的時間切成許多個Thread執行,也是這系列文探討的焦點,也是Concurrency Design的核心與由來。

但是現實總是殘酷的,這三個優化方法也讓開發者處理Concurrency時,會有快取導致的資料不一致問題

Concurrency造成的問題

可見性問題 (對應CPU的快取記憶體)

前面提到計算機架構優化第一點,因為現在每個CPU都有自己的快取記憶體,而CPU為了增快效率,會自動將資料快取記憶體中,而不回傳資料到Main Memoy。開發者在無法得知資料是否回傳到主記憶體的情況下產生BUG,這種情況在Concurrency中稱之為可見性問題(另外一個CPU無法看到資料被更動)

萬年經典Concurrency不一致範例

按照開發者的想法cnt最後應該會是20000才對,但是CPU在計算的時候將資料都佔存在各自的Cache Memory中,所以造成不一致的問題

原子性問題

而計算機架構優化第二點,OS將資源切分成Process和將時間切成Thread,讓每個Thread可以項任務依樣在CPU中分享時間不斷切換,而切換時間在OS中也稱為時間分片

但這造成了原子性問題,每個程式碼對於開發者,每一句都是具有原子性(不可切割),但因為CPU有自己瞭解的語言(組合語言),高等語言都會被轉換成組合語言,像是 cnt += 1這段程式碼轉換成組語可能就會變成

  • cnt加載到CPU register中(Cache)
  • register紀錄cnt + 1的結果
  • cnt變成上方的結果
  • 將cnt輸出到Main Memory中

如果在中間的某個環節,時間分片不小心被切換了,那資料就不一樣了,如下圖

Java Memory Model

白話來說,Memory Model控制著Thread在CPU中如何與Memory互動與分享彼此資料

為何要特別想出一個名詞定義這件事情?

程式碼對於記憶體不就是拿和放嘛?

前面提到的計算機演進簡單介紹,現在每個CPU都有自己個 Cache (Work memory),為了提昇CPU使用效率大多都Concurrency開發,而開發者不知道CPU何時才會把資料放回主記憶體,只好思考一系列防止資料不一致的問題方法,而這套方法也是Memory Model,他包含了

  • Thread如何lock/unlock資料
  • 決定是不是要使用Cache或是直接使用Memory

Java Memory Model

而Java Memory Model代表的就是JVM如何管理開發者撰寫的Thread邏輯與Memory的互動。目前的JVM Java Memory Model規範主要是根據JSR133(1998年發表)

JMM(Java Memory Model)主要核心概念

關鍵字

  • volatile
  • synchronized
  • final

Happen Before Order

從Happen Before Order開始

在Java官方文檔提出的HB規則總共有9項,這裡只針對和Thread與關鍵字相關的做探討

Volatile規則

  • A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

當有對Volatile資料做Write行為時,後續的Read行為一定看得到這樣的改變

Thread規則

  • A call to start() on a thread happens-before any actions in the started thread.

當在Main Thread呼叫Sub Thread時,在call Sub Thread start method前,Main Thread的資料內容對於Sub Thread都是可見的

  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

在呼叫Sub Thread join method後Sub Thread的資料改變對於Main Thread是可見的

Lock規則

  • An unlock on a monitor happens-before every subsequent lock on that monitor.

當Lock釋放後,Lock過得資料對於後面需要Lock的行為是可見的(前面+1過了那我一定看得到他+1過),

其實這些Happen Before Order,對於已經熟悉Java的老手們根本就是廢話連篇,但是要想到他剛出現的年代是1998年,那時候的CPU還不像現在一樣這樣強大,Java Memory Model作為支持Concurrency的先驅,實做出這樣得規範值得提出來致敬一下(許多也支持Concurrency的語言大多在Java Memory Model成功後開始也發展自己的Memory Model(C++/C#/Golang/Python))

volatile/synchronized/final

  • Volatile

前面的HB Order有提到被標示Volatile的資料如果發生Write Action那對於後面要Read Action的執行緒必定會看見,他是如何做到的?先前有提到每個CPU會先將資料佔存在自己的Cache Memory中,而Volatile就是告訴JVM控制CPU時這個資料直接在Main Memory做操作,這也是為何Volatile資料不適合讀寫太過於頻繁會降低效能(少了Cache速度的加持)

  • Synchronized

Java中最基本的同步修飾,一樣HB Order保證了在這修飾字後的Action都會受到順序的保護,Synchronized和未來介紹的Lock有點不大同的地方在於,Synchronized稱之為隱性鎖,Lock是由Java Compiler幫我們自動加與解鎖

  • Final

Final對於開發者來說就是一個宣告變數附值後不可在改變的修飾詞,但是對於JVM來說代表的是JVM可以對這樣的資料做CPU & Memory的讀取優化(因為附值後不會在改變,那我可以改變他的讀取方式)

Result

這篇文章我們討論了,Concurrency的出現是為了解決CPU/Memory/IO Device之間速度差異導致CPU閒置的問題,而Concurrency的出現讓CPU的使用更有效率,但也造成開發者要解的問題越來越困難,其中造成的原因資料原子性與可見性還有編譯語言排序。

在後半段討論了Memory Model與Java Memory Model,介紹了Happen-Before Order和Java Memory Model相關的三個修飾詞。

在後續的文章會開始針對Java Concurrency做實做的探討,這篇只算是個開門文章,當作歷史讀就好。

如果讀者們對Java Concurrency有興趣的話歡迎閱讀,如果有其他想法也歡迎留言攻擊我

Resource

Latency Time(Base for 2012)

JSR 133

Java Memory Model

--

--