<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] ## Java 內存模型(JMM) Java 內存模型試圖屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致的內存訪問效果。 ### 主內存與工作內存 處理器上的寄存器的讀寫的速度比內存快幾個數量級,為了解決這種速度矛盾,在它們之間加入了高速緩存。 加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內存區域,那么多個緩存的數據可能會不一致,需要一些協議來解決這個問題。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/1195582-20180508173147029-1341787720.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/1195582-20180508173147029-1341787720.png) 所有的變量都存儲在**主內存**中,每個線程還有自己的**工作內存**,工作內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。 線程只能直接操作工作內存中的變量,不同線程之間的變量值傳遞需要通過主內存來完成。 **Java內存模型和硬件關系圖** [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/v2-4015322359279c5568263aeb7f41c36d.jpg)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/v2-4015322359279c5568263aeb7f41c36d.jpg) **Java內存模型抽象結構圖** [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/1135283-20170403195814660-1521573510.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/1135283-20170403195814660-1521573510.png) ### 內存間交互操作 Java 內存模型定義了 8 個操作來完成主內存和工作內存的交互操作。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png) * read:把一個變量的值從主內存傳輸到工作內存中 * load:在 read 之后執行,把 read 得到的值放入工作內存的變量副本中 * use:把工作內存中一個變量的值傳遞給執行引擎 * assign:把一個從執行引擎接收到的值賦給工作內存的變量 * store:把工作內存的一個變量的值傳送到主內存中 * write:在 store 之后執行,把 store 得到的值放入主內存的變量中 * lock:作用于主內存的變量,把一個變量標識為一條線程獨占狀態 * unlock:作用于主內存變量,把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定 如果要把一個變量從主內存中復制到工作內存,就需要按順尋地執行 read 和 load 操作,如果把變量從工作內存中同步回主內存中,就要按順序地執行 store 和 write 操作。Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是 read 和 load 之間,store 和 write 之間是可以插入其他指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。 **Java內存模型還規定了在執行上述8種基本操作時,必須滿足如下規則:** * 不允許 read 和 load、store 和 write 操作之一單獨出現 * 不允許一個線程丟棄它的最近 assign 的操作,即變量在工作內存中改變了之后必須同步到主內存中 * 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中 * 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load 或 assign)的變量。即就是對一個變量實施 use 和 store 操作之前,必須先執行過了 assign 和 load 操作。 * 一個變量在同一時刻只允許一條線程對其進行lock操作,lock 和 unlock必須成對出現 * 如果對一個變量執行 lock 操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行 load 或 assign 操作初始化變量的值 * 如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執行 unlock 操作;也不允許去 unlock 一個被其他線程鎖定的變量。 * 對一個變量執行 unlock 操作之前,必須先把此變量同步到主內存中(執行 store 和 write 操作)。 參考資料: [volatile關鍵字與Java內存模型(JMM) - yzwall - 博客園](https://www.cnblogs.com/yzwall/p/6661528.html) ### 內存模型三大特性 #### 1\. 原子性 * 概念 * 事物有原子性,這個概念大概都清楚,即一個操作或多個操作要么執行的過程中不被任何因素打斷,要么不執行。 * 如何實現原子性? * 通過同步代碼塊 synchronized 或者 local 鎖來確保原子性 Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執行 assign 賦值操作,這個操作就是原子性的。但是 Java 內存模型允許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操作劃分為兩次 32 位的操作來進行,即 load、store、read 和 write 操作可以不具備原子性。 有一個錯誤認識就是,int 等原子性的變量在多線程環境中不會出現線程安全問題。前面的線程不安全示例代碼中,cnt 變量屬于 int 類型變量,1000 個線程對它進行自增操作之后,得到的值為 997 而不是 1000。 為了方便討論,將內存間的交互操作簡化為 3 個:load、assign、store。 下圖演示了兩個線程同時對 cnt 變量進行操作,load、assign、store 這一系列操作整體上看不具備原子性,那么在 T1 修改 cnt 并且還沒有將修改后的值寫入主內存,T2 依然可以讀入該變量的值。可以看出,這兩個線程雖然執行了兩次自增運算,但是主內存中 cnt 的值最后為 1 而不是 2。因此對 int 類型讀寫操作滿足原子性只是說明 load、assign、store 這些單個操作具備原子性。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92-1534148019548.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92-1534148019548.png) AtomicInteger 能保證多個線程修改的原子性。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/952afa9a-458b-44ce-bba9-463e60162945-1534148027104.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/952afa9a-458b-44ce-bba9-463e60162945-1534148027104.png) 使用 AtomicInteger 重寫之前線程不安全的代碼之后得到以下線程安全實現: ~~~java public class AtomicExample { private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); } public int get() { return cnt.get(); } } ~~~ ~~~java public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicExample example = new AtomicExample(); // 只修改這條語句 final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); } ~~~ ~~~ 1000 ~~~ 除了使用原子類之外,也可以使用 synchronized 互斥鎖來保證操作的原子性。它對應的內存間交互操作為:lock 和 unlock,在虛擬機實現上對應的字節碼指令為 monitorenter 和 monitorexit。 ~~~java public class AtomicSynchronizedExample { private int cnt = 0; public synchronized void add() { cnt++; } public synchronized int get() { return cnt; } } public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicSynchronizedExample example = new AtomicSynchronizedExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); } ~~~ ~~~ 1000 ~~~ #### 2\. 可見性 可見性指當一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。Java 內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。 主要有有三種實現可見性的方式: * volatile * synchronized,對一個變量執行 unlock 操作之前,必須把變量值同步回主內存。 * final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,并且沒有發生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那么其它線程就能看見 final 字段的值。 對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,因為 volatile 并不能保證操作的原子性。 #### 3\. 有序性 有序性是指:在本線程內觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因為發生了指令重排序。 在 Java 內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。 volatile 關鍵字通過添加內存屏障的方式來禁止指令重排,即重排序時不能把后面的指令放到內存屏障之前。 也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼。 ### 指令重排序 在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。 指令重排序包括:**編譯器重排序**和**處理器重排序** 重排序分三種類型: 1. **編譯器優化的重排序**。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。 2. **指令級并行的重排序**。現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。 3. **內存系統的重排序**。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。 從 Java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序: [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/33-1534150864535.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/33-1534150864535.png) **上述的 1 屬于編譯器重排序,2 和 3 屬于處理器重排序**。這些重排序都可能會導致多線程程序出現內存可見性問題。對于編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對于處理器重排序,JMM 的處理器重排序規則會要求 Java 編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel 稱之為 memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。 JMM 屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。 #### 數據依賴性 如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。數據依賴分下列三種類型: | 名稱 | 代碼示例 | 說明 | | --- | --- | --- | | 寫后讀 | a = 1;b = a; | 寫一個變量之后,再讀這個位置。 | | 寫后寫 | a = 1;a = 2; | 寫一個變量之后,再寫這個變量。 | | 讀后寫 | a = b;b = 1; | 讀一個變量之后,再寫這個變量。 | 上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。 前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。 注意,這里所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。 #### as-if-serial語義 as-if-serial 語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和 處理器 都必須遵守 as-if-serial 語義。 為了遵守 as-if-serial 語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關系,這些操作可能被編譯器和處理器重排序。為了具體說明,請看下面計算圓面積的代碼示例: ~~~java double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C ~~~ 上面三個操作的數據依賴關系如下圖所示: [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/11.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/11.png) 如上圖所示,A 和 C 之間存在數據依賴關系,同時 B 和 C 之間也存在數據依賴關系。因此在最終執行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的結果將會被改變)。但 A 和 B 之間沒有數據依賴關系,編譯器和處理器可以重排序 A 和 B 之間的執行順序。下圖是該程序的兩種執行順序: [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/22.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/22.png) as-if-serial 語義把單線程程序保護了起來,遵守 as-if-serial 語義的編譯器,runtime 和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial 語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。 #### 程序順序規則 根據 happens- before 的程序順序規則,上面計算圓的面積的示例代碼存在三個 happens- before 關系: 1. A happens- before B; 2. B happens- before C; 3. A happens- before C; 這里的第 3 個 happens- before 關系,是根據 happens- before 的傳遞性推導出來的。 這里 A happens- before B,但實際執行時 B 卻可以排在 A 之前執行(看上面的重排序后的執行順序)。如果A happens- before B,JMM 并不要求 A 一定要在 B 之前執行。JMM 僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。這里操作 A 的執行結果不需要對操作 B 可見;而且重排序操作 A 和操作 B 后的執行結果,與操作 A 和操作 B 按 happens- before 順序執行的結果一致。在這種情況下, JMM 會認為這種重排序并不非法(not illegal),JMM 允許這種重排序。 在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,盡可能的開發并行度。編譯器和處理器遵從這一目標,從 happens- before 的定義我們可以看出,JMM 同樣遵從這一目標。 #### 重排序對多線程的影響 現在讓我們來看看,重排序是否會改變多線程程序的執行結果。請看下面的示例代碼: ~~~java class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } Public void reader() { if (flag) { // 3 int i = a * a; // 4 …… } } } ~~~ flag 變量是個標記,用來標識變量 a 是否已被寫入。這里假設有兩個線程 A 和 B,A首先執行 writer() 方法,隨后 B 線程接著執行 reader() 方法。線程 B 在執行操作 4 時,能否看到線程 A 在操作 1 對共享變量 a 的寫入? 答案是:不一定能看到。 由于操作 1 和操作 2 沒有數據依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作 3 和操作 4 沒有數據依賴關系,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作 1 和操作 2 重排序時,可能會產生什么效果?請看下面的程序執行時序圖: [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/33.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/33.png) 如上圖所示,操作 1 和操作 2 做了重排序。程序執行時,線程 A 首先寫標記變量 flag,隨后線程 B 讀這個變量。由于條件判斷為真,線程 B 將讀取變量 a。此時,變量 a 還根本沒有被線程 A 寫入,在這里多線程程序的語義被重排序破壞了! ※注:本文統一用紅色的虛箭線表示錯誤的讀操作,用綠色的虛箭線表示正確的讀操作。 下面再讓我們看看,當操作 3 和操作 4 重排序時會產生什么效果(借助這個重排序,可以順便說明控制依賴性)。下面是操作 3 和操作 4 重排序后,程序的執行時序圖: [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/44.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/44.png) 在程序中,操作 3 和操作 4 存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對并行度的影響。以處理器的猜測執行為例,執行線程 B 的處理器可以提前讀取并計算 a\*a,然后把計算結果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變量 i 中。 從圖中我們可以看出,猜測執行實質上對操作 3 和 4 做了重排序。重排序在這里破壞了多線程程序的語義! 在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是 as-if-serial 語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。 參考資料: * [深入理解Java內存模型(一)——基礎](http://www.infoq.com/cn/articles/java-memory-model-1) * [深入理解Java內存模型(二)——重排序](http://www.infoq.com/cn/articles/java-memory-model-2) ### 先行發生原則(happens-before) Happens-before 是用來指定兩個操作之間的執行順序。提供跨線程的內存可見性。 在 Java 內存模型中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必然存在 happens-before 關系。 上面提到了可以用 volatile 和 synchronized 來保證有序性。除此之外,JVM 還規定了先行發生原則,讓一個操作無需控制就能先于另一個操作完成。 主要有以下這些原則: #### 1\. 單一線程原則 > Single Thread rule 在一個線程內,在程序前面的操作先行發生于后面的操作。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/single-thread-rule-1534148720379.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/single-thread-rule-1534148720379.png) #### 2\. 管程鎖定規則 > Monitor Lock Rule 對一個鎖的解鎖(unlock ),總是 happens-before 于隨后對這個鎖的加鎖(lock) [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/monitor-lock-rule-1534148737603.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/monitor-lock-rule-1534148737603.png) #### 3\. volatile 變量規則 > Volatile Variable Rule 對一個 volatile 變量的寫操作先行發生于后面對這個變量的讀操作。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/volatile-variable-rule-1534148747964.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/volatile-variable-rule-1534148747964.png) #### 4\. 線程啟動規則 > Thread Start Rule Thread 對象的 start() 方法調用先行發生于此線程的每一個動作。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/thread-start-rule-1534148760654.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/thread-start-rule-1534148760654.png) #### 5\. 線程加入規則 > Thread Join Rule Thread 對象的結束先行發生于 join() 方法返回。 [![](https://github.com/frank-lam/fullstack-tutorial/raw/master/notes/JavaArchitecture/assets/thread-join-rule-1534148774041.png)](https://github.com/frank-lam/fullstack-tutorial/blob/master/notes/JavaArchitecture/assets/thread-join-rule-1534148774041.png) #### 6\. 線程中斷規則 > Thread Interruption Rule 對線程 interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷發生。 #### 7\. 對象終結規則 > Finalizer Rule 一個對象的初始化完成(構造函數執行結束)先行發生于它的 finalize() 方法的開始。 #### 8\. 傳遞性 > Transitivity 如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那么操作 A 先行發生于操作 C。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看