線程通信與同步在并發(fā)編程中,有兩個需要處理的關(guān)鍵問題:
通信指線程之間以何種機制來交換信息,,通信機制有兩種:
同步是指程序中用于控制不同線程間操作發(fā)生的相對順序的機制。在共享內(nèi)存并發(fā)模型中,,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行,。在消息傳遞的并發(fā)模型里,,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式的 Java 內(nèi)存模型前面提到線程的通信與同步問題,,Java 線程之間的通信由 Java 內(nèi)存模型(簡稱 JMM)控制,,JMM 決定一個線程對共享變量的寫入何時對另一個線程可見 線程之間的共享變量存儲在主內(nèi)存,每個線程都一個私有的本地內(nèi)存,,本地內(nèi)存中存儲了該線程以 讀/寫 共享變量的副本 Java 內(nèi)存模型的抽象示意如圖 如果線程 A 和線程 B 之間要通信的話,,必須經(jīng)過下面兩個步驟:
這兩個步驟實際上是線程 A 向線程 B 發(fā)送消息,而且這個通信過程必須經(jīng)過主內(nèi)存,。JMM 通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,,來為 Java 程序員提供內(nèi)存可見性保證 重排序在執(zhí)行程序時,為了提高性能,,編譯器和處理器常常會對指令做重排序,。重排序可能會導致線程程序出現(xiàn)內(nèi)存可見性問題,下面分別介紹三種類型的重排序以及它們對內(nèi)存可見性的影響:
所以 Java 源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷以下三種重排序 JMM 保證內(nèi)存可見性由此可見,,JMM 不能任由重排序發(fā)生,,必須加以控制,否則會引發(fā)線程不安全問題,。為了更好地解釋 JMM 為保證內(nèi)存可見性所采取的措施,,首先介紹一些基礎(chǔ)概念 1. 數(shù)據(jù)依賴性如果兩個操作訪問同一個變量,且這兩個操作有一個為寫操作,,此時這兩個操作之間就存在數(shù)據(jù)依賴性,,只要重排序這兩個操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會被改變,。編譯器和處理器在重排序時,,會遵守數(shù)據(jù)依賴性,不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序,。數(shù)據(jù)依賴分為下列三種類型:
上面三種情況,,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會被改變 這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮 2. as-if-serial 語義as-if-serial 語義的意思是:不管怎么重排序,,(單線程)程序的執(zhí)行結(jié)果不能被改變,。編譯器、runtime 和處理器都必須遵守 as-if-serial 語義,,為了遵守 as-if-serial 語義,,編譯器和處理器不會對存在數(shù)據(jù)依賴性關(guān)系的操作做重排序 as-if-serial 語義把單線程程序保護起來,給程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的,,程序員無需擔心重排序會干擾他們,,也無需擔心內(nèi)存可見性問題 3. happens-before 原則happens-before 是 JMM 最核心的概念,對于 Java 程序員來說,,理解 happens-before 是理解 JMM 的關(guān)鍵,。 從 JDK5 開始,Java 使用新的 JSR-133 內(nèi)存模型,,JSR-133 使用 happens-before 的概念來闡述操作之間的內(nèi)存可見性,。在 JMM 中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,,那么這兩個操作之間必須存在 happens-before 關(guān)系,,這兩個操作既可以是在一個線程之內(nèi),也可以是不同線程之間 A happens-before B,,就是 A 操作先于 B 操作執(zhí)行,。當然這種說法并不準確,兩個操作之間具有 happens-before 關(guān)系,,僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,,且前一個操作按順序排在第二個操作之前 在 JMM 中定義了 happens-before 的原則如下:
有關(guān) happens-before 每一個原則的實現(xiàn),這里不再具體闡述,,只要知道有這么一回事就好了 4. 順序一致性內(nèi)存模型順序一致性內(nèi)存模型是一個被計算機科學家理想化了的理論參考模型,它為程序員提供了極強的內(nèi)存可見性保證,。順序一致性內(nèi)存模型有兩大特征:
順序一致性內(nèi)存模型為程序員提供的視圖如下: 在概念上,,順序一致性模型有一個單一的全局內(nèi)存,這個內(nèi)存通過一個左右擺動的開關(guān)可以連接到任意一個線程,。同時,,每一個線程必須按程序的順序來執(zhí)行內(nèi)存讀/寫操作。從上圖我們可以看出,,在任意時間點最多只能有一個線程可以連接到內(nèi)存,。當多個線程并發(fā)執(zhí)行時,,圖中的開關(guān)裝置能把所有線程的所有內(nèi)存 讀/寫 操作串行化 為了更好的理解,下面我們通過兩個示意圖來對順序一致性模型的特性做進一步的說明 假設(shè)有兩個線程 A 和 B 并發(fā)執(zhí)行,,其中 A 線程有三個操作,,它們在程序中的順序是:A1 -> A2 -> A3,B 線程也有三個操作,,它們在程序中的順序是:B1 -> B2 -> B3 假設(shè)這兩個線程使用監(jiān)視器來正確同步:A 線程的三個操作執(zhí)行后釋放監(jiān)視器,,隨后 B 線程獲取同一個監(jiān)視器。那么程序在順序一致性模型中的執(zhí)行效果將如下圖所示: 現(xiàn)在再假設(shè)這兩個線程沒有做同步,,下面是這個未同步程序在順序一致性模型中的執(zhí)行示意圖: 未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無序的,,但所有線程都只能看到一個一致的整體執(zhí)行順序。以上圖為例,,線程 A 和 B 看到的執(zhí)行順序都是:B1->A1->A2->B2->A3->B3,。之所以能得到這個保證是因為順序一致性內(nèi)存模型中的每個操作必須立即對任意線程可見 5. 總結(jié)由于重排序的存在,JMM 不可能實現(xiàn)順序一致性內(nèi)存模型,,同時也不可能完全禁止重排序,,因為這樣會影響效率。一方面,,程序員希望內(nèi)存模型易于理解,、易于編程,希望基于一個強內(nèi)存模型來編寫代碼,;另一方面,,編譯器和處理器希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來提高性能,,編譯器和處理器希望實現(xiàn)一個弱內(nèi)存模型,。這兩個因素相互矛盾,所以關(guān)鍵在于找到一個平衡點 平衡的關(guān)鍵在于優(yōu)化重排序規(guī)則,,根據(jù)前面提到的 happens-before 原則,、數(shù)據(jù)依賴性以及 as-if-serial 原則等規(guī)定了編譯器和處理器什么情況允許重排序,什么情況不允許重排序,。對于會改變程序執(zhí)行結(jié)果的重排序,,JMM 要求編譯器和處理器必須禁止這種重排序,否則不作要求,。于是程序員所看到的就是一個保證了內(nèi)存可見性的可靠的內(nèi)存模型 下圖是 JMM 的設(shè)計示意圖 從上圖我們也可以發(fā)現(xiàn),,JMM 會遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都可以,。例如,,如果編譯器經(jīng)過細致地分析后,認定一個鎖只會被單個線程訪問,,那么這個鎖可以被消除,。再如,,如果編譯器經(jīng)過細致的分析后,認定一個 volatile 變量只會被單個線程訪問,,那么編譯器可以把這個 volatile 變量當作一個普通變量來對待,。這些優(yōu)化既不會改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率,。而從程序員的角度來看,,程序員其實并不關(guān)心重排序是否真的發(fā)生,程序員關(guān)心的是只程序執(zhí)行時的語義不能被改變而已 |
|