要編寫正確的併發程序,關鍵在於:
在訪問共享的可變狀態時,須要進行正確的管理。
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) { Thread.yield(); System.out.println(number); } } } public static void main(String[] args) { new ReaderThread().start(); new ReaderThread().start(); new ReaderThread().start(); number = 42; ready = true; } }
這段代碼可能出現的結果java
在沒有同步的狀況下,編譯器,處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整,在缺少足夠同步的多線程程序中,要想對內存操做執行順序進行判斷,幾乎沒法得出正確的結論。
當ReaderThread查看ready變量時,可能會獲得一個已經失效的值,並且失效值可能不會同時出現:一個線程可能得到了某個變量的最新值,而得到了另外一個變量的失效值。
最低安全性:在沒有進行同步時讀取某個變量,可能會獲得一個失效值,但這個值至少是由以前某個線程設置的,而非隨機值。這種安全性保證也被稱爲最低安全性。
非volatile類型的64位數值變量(double和long)
因爲Java內存模型要求,變量的讀取操做和寫入操做必須都是原子操做,但對於非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位操做。
讀取volatile至關於進入同步代碼塊,寫入volatile變量至關於退出同步代碼塊。
確保它們自身狀態的可見性
,確保它們所引用對象狀態的可見性
,以及表示一些重要的聲明週期事件的發生
(例如初始化,關閉,循環退出條件等。)/** * 數綿羊 */ volatile boolean asleep; while(!asleep){ countSomeSheep(); }
加鎖機制既能保證可見性,又能夠確保原子性。而volatile變量只能保證可見性
。當且僅當知足如下全部條件時,才應該使用volatile變量:編程
發佈(publish)
一個對象,指的是對象可以在當前做用域以外的代碼中使用。數組
例如,將一個指向該對象的引用緩存
但若是在發佈時要確保線程安全性,則可能須要同步。發佈內部狀態可能會破壞封裝性,並使程序難以維持不變性條件。安全
逸出(Escape)
。public class UnsafeState { private String[] states = new String[]{ "A","B","C","D","E" }; public String[] getStates(){ return states; } }
線程封閉(Thread Confinement)
JDBC的Connection對象就使用了線程封閉技術。在典型的服務器應用程序中,線程從JDBC鏈接池中得到一個Connection對象,而且用該對象來處理請求,使用完後再將對象返回給鏈接池。服務器
Ad-hoc線程封閉是指,維護線程封閉性的職責所有由程序實現來承擔。
棧封閉是線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象。局部變量的特性之一就是封閉在執行線程中。它們位於執行線程的棧中,其餘線程沒法訪問這個棧。
維護線程封閉性的一種更規範方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get/set等訪問接口和方法,
ThreadLocal是一個建立線程局部變量的類。多線程
使用了ThreadLocal建立的變量只能被當且線程訪問,其餘線程沒法訪問和修改。併發
private void testThreadLocal() { Thread t = new Thread() { ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>(); @Override public void run() { super.run(); mStringThreadLocal.set("123"); mStringThreadLocal.get(); } }; t.start(); }
爲ThreadLocal設置初始值的話,則須要重寫initialValue
方法:框架
ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() { @Override protected String initialValue() { return Thread.currentThread().getName(); } };
本質上ThreadLocal是在堆上建立對象,可是將對象引用持有在線程的棧內存上。ide
許多事務性的框架功能,經過將事務的上下文保存在靜態的ThreadLocal對象中,當須要判斷是哪個事務時,只須要從ThreadLocal對象中讀取事務上下文便可。
知足同步需求的另外一種方法是使用不可變對象(Immutable Object)
,以前的例如獲得失效數據,丟失更新操做或者觀察到某個對象處於不一致的狀態等問題,都與多線程試圖同時訪問一個可變變量有關,若是這個變量是不可變的,那麼這些問題也就天然消失了。
不可變對象必定是線程安全的
。
當知足如下條件時,對象纔是不可變的:
關鍵字final用於構造不可變對象。final類型的域是不可修改的,但若是final域所引用的對象是可變的,那麼這些被引用的對象是能夠修改的。
在java的內存模型中,final域還有特殊的語義:final域能確保初始化過程的安全性,從而不受限制的訪問不可變對象,並在共享這些對象時無需同步。
除非須要更高的可見性,不然應將全部的域都聲明爲私有域
是個優秀的編程習慣同樣,除非須要某個域是可變的,不然都應該聲明爲final域
也是一個良好的編程習慣。在某些狀況下,咱們須要在多個線程之間共享對象,此時必須確保安全地進行共享
不能期望一個未被徹底建立的對象擁有完整性。
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) { throw new AssertionError("this statement is false"); } } }
在發佈Holder的線程發佈完成以前,Holder域是個失效值,此時的n多是空引用。
Java內存模型對不可變對象的共享提供了一種特殊的初始化安全性保證。
任何線程均可以在不須要額外同步的狀況下安全的訪問不可變對象,即便在發佈這些對象的時候沒有使用同步
若是final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍需同步。
要安全的發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全的發佈:
Hashtable
,synchonizedMap
,ConcurrentMap
中,能夠安全的將它發佈給任何從這些容器訪問它的線程(不論直接訪問仍是迭代器訪問)Vector
,CopyOnWriteArrayList
,CopyOnWriteArraySet
,SynchonizedList
,SynchonizedSet
中,能夠將元素安全地發佈到任何從這些容器中訪問該元素的線程。BlockingQueue
或者ConcurrentLinkedQueue
中,能夠將元素安全地發佈到任何從這些隊列中訪問該元素的線程。一般,要發佈一個靜態構造的對象,最簡單和最安全的方式就是使用靜態的初始化構造器。
public static Holder holder = new Holder(1);
因爲靜態初始化構造器由JVM在類的初始化階段執行,在JVM內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全的發佈。
事實不可變對象(Effectively Immutable Object)
。若是對象在構造後能夠被修改,那麼安全發佈只能保證發佈當時的可見性。對象的發佈需求取決於它的可變性:
在併發程序中使用和共享對象的時候,可使用一些實用的策略,包括:
線程封閉
:線程封閉的對象只能由一個線程擁有,對象封閉在該線程中,而且只能由這個線程修改。線程安全共享
:線程安全的對象在其內部實現同步,所以多個線程能夠經過對象的公有接口來進行訪問而不須要進一步的同步。保護對象
:被保護的對象只有經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及發佈的而且由某個特定鎖保護的對象。