1 原子性、可見性和有序性的基本概念
1.原子性(Atomicity)
由Java內存模型來直接保證的原子性變量操作包括read、load、use、assign、store和write六個,大致可以認爲基礎數據類型的訪問和讀寫是具備原子性的。如果應用場景需要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機未把lock與unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱匿地使用這兩個操作,這兩個字節碼指令反映到Java代碼中就是同步塊—synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
2.可見性(Visibility)
可見性就是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方法來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是volatile的特殊規則一般保證了新值能立即同步到主內存,以及每使用前立即從內存刷新。因爲我們可以說volatile保證了線程操作時變量的可見性,而普通變量則不能保證這一點。
除了volatile之外,Java還有兩個關鍵字能實現可見性,它們是synchronized和final。同步塊的可見性是由“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store和write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段是構造器一旦初始化完成,並且構造器沒有把“this”引用傳遞出去(即沒有造成this引用的溢出。),那麼在其它線程中就能看見final字段的值。
3.有序性(Ordering)
Java內存模型中的程序天然有序性可以總結爲一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現爲串行語義”,後半句是指“指令重排序”現象和“工作內存和主內存同步延遲”現象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
深入可參考:
深入理解Java虛擬機筆記—原子性、可見性、有序性
Stack Overflow:Allowing the this reference to escape
2 一個併發的原子性操作問題
從一個面試的問題開始:(此章節來自:Java 原子操作與併發 ,稍作修改)
調用set1()、set2()、check(),會輸出ERROR嗎?
<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> P1 { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">long</span> b = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">set1</span>() { b = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">set2</span>() { b = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">check</span>() { System.<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span>.println(b); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span> != b && <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> != b) { System.err.println(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"Error"</span>); } } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li></ul>
咋一看,調用set1、set2,b的值只可能爲0或-1,所以check裏的判斷永遠不成立,也就是不輸出“ERROR”。
其實不然,這個問題是一個併發的問題,存在兩個陷阱!
下面模擬多線程環境下調用的情況:
<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">main</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> String[] args) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> P1 v = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> P1(); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 線程 1:設置 b = 0</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> Thread t1 = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> Thread() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">run</span>() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>) { v.set1(); } }; }; t1.start(); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 線程 2:設置 b = -1</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> Thread t2 = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> Thread() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">run</span>() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>) { v.set2(); } }; }; t2.start(); <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 線程 3:檢查 0 != b && -1 != b</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> Thread t3 = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> Thread() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">run</span>() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>) { v.check(); } }; }; t3.start(); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li></ul>
使用 3 個線程分別重複執行 set1()、set2()、check()。執行輸出結果部分如下:
<code class="hljs asciidoc has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-code" style="box-sizing: border-box;">.... 0 0 1 1 1 Error Error -4294967296 0 0 4294967295 ....</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li></ul>
執行環境:
Java(TM) SE Runtime Environment (build 1.6.0_31-b05)
Java HotSpot(TM) Client VM (build 20.6-b01, mixed mode, sharing), 32bit
分析:
這道題目有兩個陷阱,分別考察了對併發執行的理解,以及對 JVM 基礎(賦值操作)的掌握。
陷阱一:併發執行
併發執行就是多個操作一起執行,CPU 執行不同上下文(可理解爲不同線程)發過來的指令。操作系統上層看上去就像是並行處理一樣。併發執行就是多個操作一起執行,CPU 執行不同上下文(可理解爲不同線程)發過來的指令。操作系統上層看上去就像是並行處理一樣。
也就是說,在編程語言層面,一個簡單的操作同樣需要考慮併發問題。
在 check() 中的 if 判斷上和設值是存在併發的,可能會出現這種情況:假設b=1,此時判斷 0 != b 爲真,再判斷1!=b時,恰好 b 被賦值爲 0 時判斷也爲真了,所以就輸出了“ERROR”。
除此外,無論 JVM、操作系統、CPU 層面對指令如何優化、重排,最終都是逐一執行單一指令,唯一不同的就是不同層面可能會對執行加以限制,
比如加入原子操作,最終保證 CPU 能夠完整執行一組指令。
陷阱二:JVM 賦值操作
注意有些賦值操作不是原子性的!
Java 基本類型中,long 和 double 是 64 位長的。32 位架構 CPU 的算術邏輯單元(ALU)寬度是 32 位的,在處理大於 32 位操作時需要處理兩次,即讀取高32位和低32位。這就說明了爲什麼輸出了4294967295和-4294967296的原因了。
Java 已經是封裝底層細節很好的語言了,但依然需要注意這些陷阱,可以使用併發處理包 java.util.concurrent.atomic 中包含了一系列無鎖原子操作類型。
也可以使用 volatile 關鍵字保證線程間變量的可見性。當然時就使用的時候還是要注意其他問題,比如count++操作的併發問題等。
深入閱讀:
聊聊併發(一)——深入分析Volatile的實現原理
Java 理論與實踐: 正確使用 Volatile 變量
多核線程筆記-volatile原理與技巧
3 併發庫的原子性操作
java.util.concurrent.atomic包可以對基本數據類型、數組中的基本數據類型和類中的基本數據類型等進行操作。可將 volatile 值、字段和數組元素的概念擴展到那些也提供原子條件更新操作的類。包中的類可以分成4組:
標量類(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
複合變量類:AtomicMarkableReference,AtomicStampedReference
內部實現原理是通過Unsafe類的方法來實現原子性操作的:(sun並沒有給出Unsafe的源代碼),此處不再詳解。
<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">import</span> sun.misc.Unsafe; ... <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> Unsafe unsafe = Unsafe.getUnsafe(); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * Atomically increments by one the current value. * *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> the updated value */</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> <span class="hljs-title" style="box-sizing: border-box;">incrementAndGet</span>() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> unsafe.getAndAddInt(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>, valueOffset, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>) + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>; }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li></ul>