在JAVA中ArrayList如何保證執行緒安全
保證執行緒安全的三種方法:
不要跨執行緒訪問共享變數
使共享變數是final型別的
將共享變數的操作加上同步
一開始就將類設計成執行緒安全的, 比在後期重新修復它,更容易.
編寫多執行緒程式, 首先保證它是正確的, 其次再考慮效能.
無狀態或只讀物件永遠是執行緒安全的.
不要將一個共享變數裸露在多執行緒環境下(無同步或不可變性保護)
多執行緒環境下的延遲載入需要同步的保護, 因為延遲載入會造成物件重複例項化
對於volatile宣告的數值型別變數進行運算, 往往是不安全的(volatile只能保證可見性,不能保證原子性).詳見volatile原理與技巧中, 髒資料問題討論.
當一個執行緒請求獲得它自己佔有的鎖時(同一把鎖的巢狀使用), 我們稱該鎖為可重入鎖.在jdk1.5併發包中, 提供了可重入鎖的java實現-ReentrantLock.
每個共享變數,都應該由一個唯一確定的鎖保護.建立與變數相同數目的ReentrantLock, 使他們負責每個變數的執行緒安全.
雖然縮小同步塊的範圍, 可以提升系統性能.但在保證原子性的情況下, 不可將原子操作分解成多個synchronized塊.
在沒有同步的情況下, 編譯器與處理器執行時的指令執行順序可能完全出乎意料.原因是, 編譯器或處理器為了優化自身執行效率, 而對指令進行了的重排序(reordering).
當一個執行緒在沒有同步的情況下讀取變數, 它可能會得到一個過期值, 但是至少它可以看到那個執行緒在當時設定的一個真實數值. 而不是憑空而來的值. 這種安全保證, 稱之為最低限的安全性(out-of-thin-air safety)
在開發併發應用程式時, 有時為了大幅度提高系統的吞吐量與效能, 會採用這種無保障的做法.但是針對, 數值的運算, 仍舊是被否決的.
當然還有一種就是 List list = Collections.synchronizedList(new ArrayList());
volatile變數,只能保證可見性, 無法保證原子性.
某些耗時較長的網路操作或IO, 確保執行時, 不要佔有鎖.
釋出(publish)物件, 指的是使它能夠被當前範圍之外的程式碼所使用.(引用傳遞)物件逸出(escape), 指的是一個物件在尚未準備好時將它釋出.
原則: 為防止逸出, 物件必須要被完全構造完後, 才可以被髮布(最好的解決方式是採用同步)
this關鍵字引用物件逸出
例子: 在建構函式中, 開啟執行緒, 並將自身物件this傳入執行緒, 造成引用傳遞.而此時, 建構函式尚未執行完, 就會發生物件逸出了.
必要時, 使用ThreadLocal變數確保執行緒封閉性(封閉執行緒往往是比較安全的, 但一定程度上會造成效能損耗)封閉物件的例子在實際使用過程中, 比較常見, 例如 hibernate openSessionInView機制, jdbc的connection機制.
單一不可變物件往往是執行緒安全的(複雜不可變物件需要保證其內部成員變數也是不可變的)良好的多執行緒程式設計習慣是: 將所有的域都宣告為final, 除非它們是可變的
保證共享變數的釋出是安全的a, 通過靜態初始化器初始化物件(jls 12.4.2敘述, jvm會保證靜態初始化變數是同步的) b, 將物件申明為volatile或使用AtomicReference c, 保證物件是不可變的d, 將引用或可變操作都由鎖來保護
設計執行緒安全的類, 應該包括的基本要素: a, 確定哪些是可變共享變數b, 確定哪些是不可變的變數c, 指定一個管理併發訪問物件狀態的策略
將資料封裝在物件內部, 並保證對資料的訪問是原子的.建議採用volatile javabean模型或者構造同步的getter,setter.
執行緒限制性使構造執行緒安全的類變得更容易, 因為類的狀態被限制後, 分析它的執行緒安全性時, 就不必檢查完整的程式.
編寫併發程式, 需要更全的註釋, 更完整的文件說明.
在需要細分鎖的分配時, 使用java監視器模式好於使用自身物件的監視器鎖.前者的靈活性更好.
Object target = new Object();
// 這裡使用外部物件來作為監視器, 而非this
synchronized(target) {
// TODO
}
針對java monitor pattern, 實際上ReentrantLock的實現更易於併發程式設計.功能上, 也更強大.
設計併發程式時, 在保證伸縮性與效能折中的前提下, 優先考慮將共享變數委託給執行緒安全的類.由它來控制全域性的併發訪問.
使用普通同步容器(Vector, Hashtable)的迭代器, 需要外部鎖來保證其原子性.原因是, 普通同步容器產生的迭代器是非執行緒安全的.
在併發程式設計中, 需要容器支援的時候, 優先考慮使用jdk併發容器(ConcurrentHashMap, ConcurrentLinkedQueue, CopyOnWriteArrayList...).
ConcurrentHashMap, CopyOnWriteArrayList併發容器的迭代器,以及全範圍的size(), isEmpty() 都表現出弱一致性.他們只能標示容器當時的一個數據狀態. 無法完整響應容器之後的變化和修改.
使用有界佇列, 在佇列充滿或為空時, 阻塞所有的讀與寫操作. (實現生產-消費的良好方案) BlockQueue下的實現有LinkedBlockingQueue與ArrayBlockingQueue, 前者為連結串列, 可變操作頻繁優先考慮,後者為陣列, 讀取操作頻繁優先考慮. PriorityBlockingQueue是一個按優先順序順序排列的阻塞佇列, 它可以對所有置入的元素進行排序(實現Comparator介面)
當一個方法, 能丟擲InterruptedException, 則意味著, 這個方法是一個可阻塞的方法, 如果它被中斷, 將提前結束阻塞狀態.當你呼叫一個阻塞方法, 也就意味著, 本身也稱為了一個阻塞方法, 因為你必須等待阻塞方法返回.
如果阻塞方法丟擲了中斷異常, 我們需要做的是, 將其往上層拋, 除非當前已經是需要捕獲異常的層次.如果當前方法, 不能丟擲InterruptedException, 可以使用Thread.currentThread.interrupt()方法, 手動進行中斷.
在當前的Java記憶體模型下,執行緒可以把變數儲存在本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。
要解決這個問題,只需要像在本程式中的這樣,把該變數宣告為volatile(不穩定的)即可,這就指示JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。一般說來,多工環境下各任務間共享的標誌都應該加volatile修飾。
Volatile修飾的成員變數在每次被執行緒訪問時,都強迫從共享記憶體中重讀該成員變數的值。而且,當成員變數發生變化時,強迫執行緒將變化值回寫到共享記憶體。這樣在任何時刻,兩個不同的執行緒總是看到某個成員變數的同一個值。
Java語言規範中指出:為了獲得最佳速度,允許執行緒儲存共享成員變數的私有拷貝,而且只當執行緒進入或者離開同步程式碼塊時才與共享成員變數的原始值對比。
這樣當多個執行緒同時與某個物件互動時,就必須要注意到要讓執行緒及時的得到共享成員變數的變化。
而volatile關鍵字就是提示VM:對於這個成員變數不能儲存它的私有拷貝,而應直接與共享成員變數互動。
使用建議:在兩個或者更多的執行緒訪問的成員變數上使用volatile。當要訪問的變數已在synchronized程式碼塊中,或者為常量時,不必使用。
由於使用volatile遮蔽掉了VM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。