阿里一道Java併發面試題 (詳細分析篇)

題目

我我的一直認爲:網絡、併發相關的知識,相對其餘一些編程知識點更難一些,主要是很差調試而且涉及內容太多 !程序員

因此今天就取一篇併發相關的內容分享下,我相信你們認真看完會有收穫的。編程

你們能夠先看看這個問題,看看這個是否有問題呢? 那裏有問題呢?安全

若是你在這個問題上面停留超過5s的話,那麼表示你對這塊某些知識還有點模糊,須要再鞏固下,下面咱們一塊兒來分析下!性能優化

結論

多線程併發的同時進行set、get操做, A線程調用set方法,B線程並必定能對這個改變可見!!!網絡

分析

這個類很是簡單,裏面有一個屬性,有2個方法:get、set方法,一個用來設置屬性值,一個用來獲取屬性值,在設置屬性方法上面加了synchronized。多線程

隱式信息:多線程併發的同時進行set、get操做, A線程調用set方法,B線程能夠裏面感知到嗎???架構

說到這裏, 問題就變成了synchronized在剛剛說的上下文下面可否保證可見性!!!併發

關鍵詞synchronized的用法

指定加鎖對象:對給定對象加鎖,進入同步代碼前須要得到給定對象的鎖。app

直接做用於實例方法:至關於對當前實例加鎖,進入同步代碼前要得到當前實例的鎖。分佈式

直接做用於靜態方法:至關於對當前類加鎖,進入同步代碼前要得到當前類的鎖。

synchronized它的工做就是對須要同步的代碼加鎖,使得每一次只有一個線程能夠進入同步塊(實際上是一種悲觀策略)從而保證線程之間得安全性。

從這裏咱們能夠知道,咱們須要分析的屬於第二類狀況,也就是說多個線程若是同時進行set方法的時候,因爲存在鎖,因此會一個一個進行set操做,而且是線程安全的,可是get方法並無加鎖,表示假如A線程在進行set的同時B線程能夠進行get操做。而且能夠多個線程同時進行get操做,可是同一時間最多隻能有一個set操做。

Java 內存模型 happens-before原則

JSR-133 內存模型使用 happens-before 的概念來闡述操做之間的內存可見性。在 JMM 中,若是 一個操做執行的結果須要對另外一個操做可見 ,那麼這兩個操做之間必需要存在 happens-before 關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。

與程序員密切相關的 happens-before 規則以下:

程序順序規則:一個線程中的每一個操做,happens-before 於該線程中的任意後續操做。

監視器鎖規則:對一個監視器的解鎖,happens-before 於隨後對這個監視器的加鎖。

volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意後續對這個 volatile 域的讀。

傳遞性:若是 A happens-before B,且 B happens-before C,那麼 A happens-before C。

注意,兩個操做之間具備 happens-before 關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before 僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first is visible to and ordered before the second)。

其中有 監視器鎖規則:對一個監視器的解鎖,happens-before 於隨後對這個監視器的加鎖。這一條,僅僅只是針對synchronized的set方法,而對於get並無這方面的說明。

其實在這種上下文下面一個synchronized的set方法,一個普通的get方法,a線程調用set方法,b線程並必定能對這個改變可見!

volatile

volatile可見性

前面happens-before原則就提到: volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意後續對這個 volatile 域的讀。 volatile從而保證了多線程下的可見性!!!

volatile 禁止內存重排序

下面是 JMM 針對編譯器制定的 volatile 重排序規則表:

爲了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

下面是基於保守策略的 JMM 內存屏障插入策略:

在每一個 volatile 寫操做的前面插入一個 StoreStore 屏障。

在每一個 volatile 寫操做的後面插入一個 StoreLoad 屏障。

在每一個 volatile 讀操做的後面插入一個 LoadLoad 屏障。

在每一個 volatile 讀操做的後面插入一個 LoadStore 屏障。

下面是保守策略下,volatile 寫操做 插入內存屏障後生成的指令序列示意圖:

下面是在保守策略下,volatile 讀操做 插入內存屏障後生成的指令序列示意圖:

上述 volatile 寫操做和 volatile 讀操做的內存屏障插入策略很是保守。在實際執行時,只要不改變 volatile 寫-讀的內存語義,編譯器能夠根據具體狀況省略沒必要要的屏障。

模擬

經過上面的分析,其實這個題目涉及到的內容都提到了,而且進行了解答。

雖然你知道的緣由,可是想模擬並非一件容易的事情!,下面咱們來模擬看看效果:

publicclassThreadSafeCache{intresult;publicintgetResult(){returnresult;    }publicsynchronizedvoidsetResult(intresult){this.result = result;    }publicstaticvoidmain(String[] args){        ThreadSafeCache threadSafeCache =newThreadSafeCache();for(inti =0; i <8; i++) {newThread(() -> {intx =0;while(threadSafeCache.getResult() <100) {                    x++;                }                System.out.println(x);            }).start();        }try{            Thread.sleep(1000);        }catch(InterruptedException e) {            e.printStackTrace();        }        threadSafeCache.setResult(200);    }}

效果:

程序會一直卡在這邊不動,表示set修改的200,get方法並不可見!!!

添加volatile 關鍵詞觀察效果

其實例子中synchronized關鍵字能夠去掉,僅僅用volatile便可。

效果:

代碼很快正常結束了!

加架構羣:705127209 領取資料,裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的資料

結論:

多線程併發的同時進行set、get操做, A線程調用set方法,B線程並必定能對這個改變可見!!! ,上面的代碼中,若是對get方法也加synchronized也是可見的,仍是happens-before的 監視器鎖規則:對一個監視器的解鎖,happens-before 於隨後對這個監視器的加鎖。 ,只是volatile比synchronized更輕量級,因此本例直接用volatile。可是對於符合原子操做i++這裏仍是不行的仍是須要synchronized。

相關文章
相關標籤/搜索