第十六章:Java内存模型——Java并发编程实战
一、什么是內存模型,為什么要使用它
如果缺少同步,那么將會有許多因素使得線程無法立即甚至永遠看到一個線程的操作結果
- 編譯器把變量保存在本地寄存器而不是內存中
- 編譯器中生成的指令順序,可以與源代碼中的順序不同
- 處理器采用亂序或并行的方式來執行指令
- 保存在處理器本地緩存中的值,對于其他處理器是不可見
在單線程中,只要程序的最終結果與在嚴格串行環境中執行的結果相同,那么上述所有操作都是允許的
在多線程中,JVM通過同步操作來找出這些協調操作將在何時發生
JMM規定了JVM必須遵循一組最小保證,這組保證規定了對變量的寫入操作在何時將對其他線程可見
1、平臺的內存模型
每個處理器都擁有自己的緩存,并且定期地與主內存進行協調,在不同的處理器架構中提供了不同級別的緩存一致性,即允許不同的處理器在任意時刻從同一個存儲位置上看到不同的值。JVM通過在適當的位置上插入內存柵欄來屏蔽在JMM與底層平臺內存模型之間的差異。Java程序不需要指定內存柵欄的位置,而只需通過正確地使用同步來找出何時將訪問共享狀態
2、重排序
各種使操作延遲或者看似亂序執行的不同原因,都可以歸為重排序,內存級的重排序會使程序的行為變得不可預測
1 Thread one = new Thread(new Runnable() { 2 public void run() { 3 a = 1; 4 x = b; 5 } 6 });上述代碼也會有以下結果:
3、Java內存模式簡介
Java內存模型是通過各種操作來定義的,包括變量的讀/寫操作,監視器的加鎖和釋放操作,以及線程的啟動和合并操作
JMM為程序中所有的操作定義了一個偏序關系,稱為Happens-Before,使在正確同步的程序中不存在數據競爭(缺乏Happens-Before關系,那么JVM可以對它們任意地重排序)
- 程序順序規則。如果程序中操作A在操作B之前,那么在線程中A操作將在B操作之前執行
- 監視器鎖規則。在監視器鎖上的解鎖操作必須在同一個監視器鎖上的加鎖操作之前執行。(顯式鎖和內置鎖在加鎖和解鎖等操作上有著相同的內存語義)?
- volatile變量規則。對volatile變量的寫入操作必須在對該變量的讀操作之前執行。(原子變量與volatile變量在讀操作和寫操作上有著相同的語義)?
- 線程啟動規則。在線程上對Thread.start的調用必須在該線程中執行任何操作之前執行
- 線程結束規則。線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執行,或者從Thread.join中成功返回,或者在調用Thread.isAlive時返回false
- 中斷規則。當一個線程在另一個線程上調用interrupt時,必須在被中斷線程檢測到interrupt調用之前執行(通過拋出InterruptException,或者調用isInterrupted和interrupted)
- 終結器規則。對象的構造函數必須在啟動該對象的終結器之前執行完成
- 傳遞性。如果操作A在操作B之前執行,并且操作B在操作C之前執行,那么操作A必須在操作C之前執行。
4、借助同步
”借助(Piggyback)“現有同步機制的可見性屬性,對某個未被鎖保護的變量的訪問操作進行排序(不希望給對象加鎖,而又想維護它的順序)
?
Happens-Before排序包括:
- 將一個元素放入一個線程安全容器的操作將在另一個線程從該容器中獲得這個元素的操作之前執行
- 在CountDownLatch上的倒數操作將在線程從閉鎖上的await方法返回之前執行
- 釋放Semaphore許可的操作將在從該Semaphore上獲得一個許可之前執行
- Future表示的任務的所有操作將在從Future.get中返回之前執行
- 向Executor提交一個Runnable或Callable的操作將在任務開始執行之前執行
- 一個線程到達CyclicBarrier或Exchange的操作將在其他到達該柵欄或交換點的線程被釋放之前執行。如果CyclicBarrier使用一個柵欄操作,那么到達柵欄的操作將在柵欄操作之前執行,而柵欄操作又會在線程從柵欄中釋放之前執行
?
二、發布
造成不正確發布的真正原因:"發布一個共享對象"與"另一個線程訪問該對象"之間缺少一種Happens-Before的關系
1、不安全的發布
除了不可變對象以外,使用被另一個線程初始化的對象通常都是不安全的,除非對象的發布操作是在使用該對象的線程開始使用之前執行
?
1 public class UnsafeLazyInitialization { 2 private static Object resource; 3 4 public static Object getInstance(){ 5 if (resource == null){ 6 resource = new Object(); //不安全的發布 7 } 8 return resource; 9 } 10 }?
原因一:線程B看到了線程A發布了一半的對象
原因二:即使線程A初始化Resource實例之后再將resource設置為指向它,線程B仍可能看到對resource的寫入操作將在對Resource各個域的寫入操作之前發生。因為線程B看到的線程A中的操作順序,可能與線程A執行這些操作時的順序并不相同
2、安全發布
例:BlockingQueue的同步機制保證put在take后執行,A線程放入對象能保證B線程取出時是安全的
借助于類庫中現在的同步容器、使用鎖保護共享變量、或都使用共享的volatile類型變量,都可以保證對該變量的讀取和寫入是按照happens-before排序的
happens-before事實上可以比安全發布承諾更強的可見性與排序性
3、安全初始化模式
方式一:加鎖保證可見性與排序性,存在性能問題
1 public class UnsafeLazyInitialization { 2 private static Object resource; 3 4 public synchronized static Object getInstance(){ 5 if (resource == null){ 6 resource = new Object(); //不安全的發布 7 } 8 return resource; 9 } 10 }?
方式二:提前初始化,可能造成浪費資源
1 public class EagerInitialization { 2 private static Object resource = new Object(); 3 public static Object getInstance(){ 4 return resource; 5 } 6 }方式三:延遲初始化,建議
1 public class ResourceFactory { 2 private static class ResourceHolder{ 3 public static Object resource = new Object(); 4 } 5 public static Object getInstance(){ 6 return ResourceHolder.resource; 7 } 8 }方式四:雙重加鎖機制,注意保證volatile類型,否則出現一致性問題
1 public class DoubleCheckedLocking { 2 private static volatile Object resource; 3 public static Object getInstance(){ 4 if (resource == null){ 5 synchronized (DoubleCheckedLocking.class){ 6 if (resource == null){ 7 resource = new Object(); 8 } 9 } 10 } 11 return resource; 12 } 13 }?
三、初始化過程中的安全性
- 如果能確保初始化過程的安全性,被正確構造的不可變對象在沒有同步的情況下也能安全地在多個線程之間共享
- 如果不能確保初始化的安全性,一些本應為不可變對象的值將會發生改變
初始化安全性只能保證通過final域可達的值從構造過程完成時可見性。對于通過非final域可達的值,或者在構成過程完成后可能改變的值,必須采用同步來確保可見性
?
轉載于:https://www.cnblogs.com/HectorHou/p/6054302.html
總結
以上是生活随笔為你收集整理的第十六章:Java内存模型——Java并发编程实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php barcode设置黑条宽度,打印
- 下一篇: java线程池 锁_java多线程——锁