官术网_书友最值得收藏!

1.5 原子性、內存可見性和重排序——重新認識synchronized和volatile

對于涉及共享變量(Shared Variable)訪問的操作,若該操作從其執行線程以外的任意線程來看是不可分割的,那么該操作就是原子操作(Atomic Operation),相應地我們稱該操作具有原子性(Atomicity)。例如,對int型共享變量counter執行counter++操作就不是原子操作,這是因為counter++實際上可以分解為3個子操作:

? 將變量counter的當前值加載(讀取)到寄存器R中。

? 將寄存器R的值增加1。

? 將寄存器R的值寫入變量counter。

在多線程環境中,非原子操作可能會受其他線程的干擾。比如,上述例子如果沒有對相應的代碼進行同步(Synchronization)處理,則在執行第2個子操作的時候可能出現counter值已經被其他線程修改的情況,此時這一步的操作所使用的counter變量的“當前值”其實是過期的。當然,synchronized關鍵字可以幫助我們實現操作的原子性,以避免這種線程間干擾的情況。

synchronized關鍵字可以實現操作的原子性,其本質是通過該關鍵字所限定的臨界區(Critical Section)的排他性來保證在任一時刻只有一個線程能夠執行臨界區中的代碼,這使得臨界區中的代碼代表了一個原子操作。關于這一點,讀者可能已經很清楚。但是,synchronized關鍵字所起的另一個作用——保障內存可見性(Memory Visibility),也值得我們回顧。

CPU在執行代碼的時候,為了降低變量訪問的時間開銷,可能將代碼中訪問的變量值緩存到該CPU的高速緩存(如L1 Cache、L2 Cache等)中。因此當相應代碼再次訪問某個變量時,相應的值可能是從CPU的高速緩存而不是主內存中讀取的。同樣地,出于對內存訪問效率的考慮,代碼對變量值的修改也可能僅被寫入執行這段代碼的CPU上的寫緩沖器(Store Buffer)里,而沒有被寫入該CPU的高速緩存里,更沒有被寫入主內存里。由于每個CPU都有自己的高速緩存,而一個CPU并不能直接讀取其他CPU上的高速緩存里的內容,這就導致一個線程對共享變量所做的更新可能無法被其他CPU上運行的其他線程“看到”。這就是所謂的內存可見性。

synchronized關鍵字的另一個作用就是,它保證了一個線程執行臨界區中的代碼時所修改的變量值對于稍后執行該臨界區中的代碼的線程來說是可見的。這對于保證多線程代碼的正確性來說非常重要。

而volatile關鍵字也能夠保證內存可見性,即一個線程對一個采用volatile關鍵字修飾的變量的值的更改對于其他訪問該變量的線程而言總是可見的。也就是說,其他線程不會讀到一個“過期”的變量值。因此,有人將volatile關鍵字與synchronized關鍵字所代表的內部鎖做了比較,將其稱為輕量級的鎖。這種稱呼其實并不恰當,volatile關鍵字只能保證內存可見性,它并不能像synchronized關鍵字所代表的內部鎖那樣保證操作的原子性。volatile關鍵字保障內存可見性的核心機制是,當一個線程修改了一個volatile關鍵字修飾的變量的值時,該值會被寫入當前線程所在的CPU上的高速緩存里,而不是僅僅停留在該CPU的寫緩沖器里,而其他CPU上的高速緩存里存儲的該變量的值(副本)也會因此而失效。這就保證了這些其他線程再訪問該volatile關鍵字修飾的變量時總是可以通過處理器的緩存一致性協議(Coherence Protocol)來獲取該變量的最新值。

volatile關鍵字的另一個作用是禁止了指令重排序(Re-order)[10]。編譯器和CPU為了提高指令的執行效率可能會進行指令重排序,這使得代碼的實際執行方式可能不是按照我們所認為的方式(源代碼所指定的順序)進行的。例如,下面的實例變量初始化語句

所做的事情非常簡單:

1)創建類SomeClass的實例。

2)初始化SomeClass實例。

3)將類SomeClass實例的引用賦值給變量someObject。

但是由于指令重排序的作用,這段代碼的實際執行順序可能是:

1)創建SomeClass類的實例。

2)將對SomeClass實例的引用賦值給變量someObject。

3)初始化SomeClass實例(即執行SomeClass類的構造器)。

因此,當其他線程訪問someObject變量的值時,其得到的僅是指向一段存儲SomeClass實例的內存空間的引用而已,而該內存空間相應的SomeClass實例的初始化可能尚未完成,這就可能導致一些意想不到的結果。而禁止指令重排序則可以使上述代碼按照我們所期望的順序(正如代碼所表達的順序)來執行。

禁止指令重排序雖然導致編譯器和CPU無法對一些指令進行可能的優化,但是它在某種程度上讓代碼的執行看起來更符合我們的期望。

本書涉及的代碼也有不少地方使用了volatile關鍵字。讀者需要注意這個關鍵字對多線程代碼的正確性所起的作用。

與volatile相比,synchronized既能保證操作的原子性,又能保證內存可見性,而volatile僅能保證內存可見性。但是,synchronized會導致上下文切換,而synchronized不會。

主站蜘蛛池模板: 保德县| 昌乐县| 德钦县| 葵青区| 体育| 宜君县| 泗阳县| 梅河口市| 新和县| 潞城市| 双辽市| 永兴县| 沧州市| 吉隆县| 枣阳市| 紫金县| 曲水县| 叶城县| 贡觉县| 淳化县| 西盟| 和平区| 阿图什市| 邵阳县| 邯郸县| 铅山县| 榆林市| 清河县| 堆龙德庆县| 修水县| 承德市| 八宿县| 桦南县| 五家渠市| 永善县| 石景山区| 乐昌市| 独山县| 辽宁省| 江陵县| 峨山|