一篇文章看懂Java併發和執行緒安全(一)
【本文轉自爪哇筆記 作者:冷血狂魔 原文連結:https://mp.weixin.qq.com/s/4NM4EkeSrspI4XTo673K0w】
前言
長久以來,一直想剖析一下Java執行緒安全的本質,但是苦於有些微觀的點想不明白,便擱置了下來,前段時間慢慢想明白了,便把所有的點串聯起來,趁著思路清晰,整理成這樣一篇文章。
導讀
為什麼有多執行緒?
執行緒安全描述的本質問題是什麼?
Java記憶體模型(JMM)資料可見性問題、指令重排序、記憶體屏障
揭曉答案
為什麼有多執行緒
談到多執行緒,我們很容易與高效能畫上等號,但是並非如此,舉個簡單的例子,從1加到100,用四個執行緒計算不一定比一個執行緒來得快。因為執行緒的建立和上下文切換,是一筆巨大的開銷。
那麼設計多執行緒的初衷是什麼呢?來看一個這樣的實際例子,計算機通常需要與人來互動,假設計算機只有一個執行緒,並且這個執行緒在等待使用者的輸入,那麼在等待的過程中,CPU什麼事情也做不了,只能等待,造成CPU的利用率很低。如果設計成多執行緒,在CPU在等待資源的過程中,可以切到其他的執行緒上去,提高CPU利用率。
現代處理器大多含有多個CPU核心,那麼對於運算量大任務,可以用多執行緒的方式拆解成多個小任務併發的執行,提高計算的效率。
總結起來無非兩點,提高CPU的利用率、提高計算效率。
執行緒安全的本質
我們先來看一個例子:
上面是一個把變數自增100次的例子,只不過用了4個執行緒,每個執行緒自增25次,用CountDownLatch等4個執行緒執行完,列印出最終結果。實際上,我們希望程式的結果是100,但是列印出來的結果並非總是100。
這就引出了執行緒安全所描述的問題,我們先用通俗的話來描述一下執行緒安全:
執行緒安全就是要讓程式執行出我們想要的結果,或者話句話說,讓程式像我們看到的那樣執行。
解釋一下我總結的這句話,我們先new出了一個add物件,呼叫了物件的doAdd方法,本來我們希望每個執行緒有序的自增25次,最終得到正確的結果。如果程式增的像我們預先設定的那樣執行,那麼這個物件就是執行緒安全的。
下面我們來看看Brian Goetz對執行緒安全的描述:當多執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那麼這個物件就是執行緒安全的。
下面我們就來分析這段程式碼為什麼不能確保總是得到正確的結果。
Java記憶體模型(JMM)資料可見性問題、指令重排序、記憶體屏障
先從計算機的硬體效率說起,CPU的計算速度比記憶體快幾個數量級,為了平衡CPU和記憶體之間的矛盾,引入的快取記憶體,每個CPU都有快取記憶體,甚至是多級快取L1、L2和L3,那麼快取與記憶體的互動需要快取一致性協議,這裡就不深入講解。那麼最終處理器、快取記憶體、主記憶體的互動關係如下:
那麼Java的記憶體模型(Java Memory Model,簡稱JMM)也定義了執行緒、工作記憶體、主記憶體之間的關係,非常類似於硬體方面的定義。
這裡順帶提一下,Java虛擬機器執行時記憶體的區域劃分
·方法區:儲存類資訊、常量、靜態變數等,各執行緒共享
·虛擬機器棧:每個方法的執行都會建立棧幀,用於儲存區域性變數、運算元棧、動態連結等,虛擬機器棧主要儲存這些資訊,執行緒私有
·本地方法棧:虛擬機器使用到的Native方法服務,例如c程式等,執行緒私有
·程式計數器:記錄程式執行到哪一行了,相當於當前執行緒位元組碼的行號計數器,執行緒私有
·堆:new出的例項物件都儲存在這個區域,是GC的主戰場,執行緒共享。
所以對於JMM定義的主記憶體,大部分時候可以對應堆記憶體、方法區等執行緒共享的區域,這裡只是概念上對應,其實程式計數器、虛擬機器棧等也有部分是放在主記憶體的,具體看虛擬機器的設計。
好了,瞭解了JMM記憶體模型,我們來分析一下,上面的程式為什麼沒得到正確的結果。請看下圖,執行緒A、B同時去讀取主記憶體的count初始值存放在各自的工作記憶體裡,同時執行了自增操作,寫回主記憶體,最終得到了錯誤的結果。
我們再來深入分析一下,造成這個錯誤的本質原因:
·可見性,工作記憶體的最新值不知道什麼時候會寫回主記憶體
·有序性,執行緒之間必須是有序的訪問共享變數,我們用“視界”這個概念來描述一下這個過程,以B執行緒的視角看,當他看到A執行緒運算好之後,把值寫回之記憶體之後,馬上去讀取最新的值來做運算。A執行緒也應該是看到B運算完之後,馬上去讀取,在做運算,這樣就得到了正確的結果。
接下來,我們來具體分析一下,為什麼要從可見性和有序性兩個方面來限定。
給count加上volatile關鍵字,就保證了可見性。
private volatile int count = 0;
volatile關鍵字,會在最終編譯出來的指令上加上lock字首,lock字首的指令做三件事情
·防止指令重排序(這裡對本問題的分析不重要,後面會詳細來講)
·鎖住匯流排或者使用鎖定快取來保證執行的原子性,早期的處理可能用鎖定匯流排的方式,這樣其他處理器沒辦法通過匯流排訪問記憶體,開銷比較大,現在的處理器都是用鎖定快取的方式,在配合快取一致性來解決。
·把緩衝區的所有資料都寫回主記憶體,並保證其他處理器快取的該變數失效
既然保證了可見性,加上了volatile關鍵詞,為什麼還是無法得到正確的結果,原因是count++,並非原子操作,count++等效於如下步驟:
·從主記憶體中讀取count賦值給執行緒副本變數:temp=count
·執行緒副本變數加1:temp=temp+1
·執行緒副本變數寫回主記憶體:count=temp
就算是真的嚴苛的給匯流排加鎖,導致同一時刻,只能有一個處理器訪問到count變數,但是在執行第(2)步操作時,其他cpu已經可以訪問count變數,此時最新運算結果還沒刷回主記憶體,造成了錯誤的結果,所以必須保證順序性。
那麼保證順序性的本質,就是保證同一時刻只有一個CPU可以執行臨界區程式碼。這時候做法通常是加鎖,鎖本質是分兩種:悲觀鎖和樂觀鎖。如典型的悲觀鎖synchronized、JUC包下面典型的樂觀鎖ReentrantLock。
總結一下:要保證執行緒安全,必須保證兩點:共享變數的可見性、臨界區程式碼訪問的順序性。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31137683/viewspace-2157558/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 一篇文章看懂Java併發和執行緒安全(二)Java執行緒
- Java併發實戰一:執行緒與執行緒安全Java執行緒
- Java多執行緒詳解——一篇文章搞懂Java多執行緒Java執行緒
- 【JAVA併發第一篇】Java的程式與執行緒Java執行緒
- Java併發(一)----程式、執行緒、並行、併發Java執行緒並行
- Java併發專題(二)執行緒安全Java執行緒
- Java併發-執行緒安全的集合類Java執行緒
- Java 併發:執行緒、執行緒池和執行器全面教程Java執行緒
- Java併發程式設計之執行緒安全、執行緒通訊Java程式設計執行緒
- 一文看懂JUC多執行緒及高併發執行緒
- 【JAVA併發第四篇】執行緒安全Java執行緒
- JAVA多執行緒和併發基礎Java執行緒
- JAVA多執行緒併發Java執行緒
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- 多執行緒與高併發(二)執行緒安全執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- Java併發(四)----執行緒執行原理Java執行緒
- Java併發(十七)----變數的執行緒安全分析Java變數執行緒
- Java執行緒(一):執行緒安全與不安全Java執行緒
- Java多執行緒和併發問題集Java執行緒
- Java併發——執行緒池ThreadPoolExecutorJava執行緒thread
- Java併發系列 — 執行緒池Java執行緒
- Java併發專題(一)認識執行緒Java執行緒
- Java高併發與多執行緒(一)-----概念Java執行緒
- java多執行緒與併發 - 執行緒池詳解Java執行緒
- 和朱曄一起復習Java併發(一):執行緒池Java執行緒
- Java併發程式設計:Java執行緒Java程式設計執行緒
- java多執行緒與併發 - 併發工具類Java執行緒
- [Java併發]執行緒的並行等待Java執行緒並行
- Java併發(十六)----執行緒八鎖Java執行緒
- Java執行緒的併發工具類Java執行緒
- 啃碎併發(五):Java執行緒安全特性與問題Java執行緒
- 併發程式設計與執行緒安全程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒的由來(一)Java程式設計執行緒
- Java8 新特性併發篇(一) | 執行緒與執行器Java執行緒
- 面試題-關於Java執行緒池一篇文章就夠了面試題Java執行緒
- java併發筆記之java執行緒模型Java筆記執行緒模型
- Java併發指南1:併發基礎與Java多執行緒Java執行緒