Effective java8——執行緒安全級別和延遲初始化
Java中執行緒安全級別:
1、不可變的(immutable):
不可變的類的例項是不能被修改的,每個例項中包含的所有資訊都必須在建立該例項的時候就提供,並在物件的整個生命週期內固定不變。不可變類不需要外部的同步,常見的例子有String,long和BigInteger。
為了使類成為不可變,需要遵循下面五條規則:
(1)不提供任何會修改物件狀態的方法(如setter方法等)。
(2)保證類不會被擴充套件,最常用的做法是將類宣告為final型別。
(3)使所有的域都是final。
(4)使所有的域都是私有的。
(5)確保對於任何可變元件的互斥訪問。
如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲得指向這些可變物件的引用,且永遠不要用客戶端提供的物件引用來初始化這樣的域,不要從任何訪問方法中返回該物件的引用,在構造器和訪問方法中使用保護性拷貝。
下面使用一個複數類來演示不可變類:
public final class Complex{
private final double re;//複數的實部
private final double im;//複數的虛部
public Complex(double re,double im){
this.re = re;
this.im= im;
}
public double realPart(){
return re;
}
public double imaginaryPart(){
return im;
}
public Complex add(Complex c){
return new Complex(re + c.re,im + c.im);
}
public Complex substract(Complex c){
return new Complex(re -c.re,im-c.im);
}
public Complex multiply(Complex c){
return new Complex(re*c.re - im*c.im,re*c.im + im*c.re);
}
public Complex divide(Complex c){
double tmp = c.re*c.re + c.im*c.im;
return new Cmplex((re* c.re + im*c.im)/tmp,(im*c.re-re*c.im)/tmp);
}
}
2、無條件的執行緒安全:
無條件的執行緒安全類的例項時可變的,但是這個類有著足夠的內部同步,所以它的例項可以被併發使用,無需任何外部同步。JDK中的Random和ConcurrentHashMap就是無條件執行緒安全的類。
3、有條件的執行緒安全:
有條件的執行緒安全類的例項除了有些方法為了進行安全的併發使用而需要外部同步之外,這種執行緒安全級別與無條件的執行緒安全相同。JDK中Collections.synchronized包裝的同步集合,Vector,HashTable,StringBuffer等就是有條件的執行緒安全類。
Collections.synchronized使用裝飾者模式來包裝非執行緒安全的集合容器,在這些非執行緒安全集合容器的公共方法上新增synchronized同步關鍵字,將方法變成執行緒安全。
有條件的執行緒安全集合在多執行緒操作時一般不需要同步,但是在遍歷(集合產生的迭代器也必須要求外部同步)的時候必須要求外部同步,否則如果遍歷過程中涉及到新增或者刪除等改變集合容器大小的操作時可能會產出例如ArrayIndexOutOfBoundException。
有條件執行緒安全的例子:
Map<k,v> m = Collections.syschronizedMap(new HashMap<k,v>());
....
Set<k> s = m.keySet();
....
synchronized(m){
for(k key : s){
key.f();
....
}
}
4、非執行緒安全:
非執行緒安全類的例項是可變的,為了併發地使用這些類,呼叫者必須使用外部同步包圍每個呼叫序列。JDK中ArrayList,HashMap,StringBuilder等都是非執行緒安全的例子。
只有在多執行緒共享的情況下需要對非執行緒安全類進行外部同步,如果非執行緒安全的類不會被多個執行緒共享,則同樣不需要考慮執行緒安全的同步問題。
5、執行緒對立:
即使所有的方法呼叫都被外部同步包圍,執行緒對立類也不能安全地被多個執行緒併發地使用。
執行緒對立的根源在於沒有同步地修改靜態資料,JDK中的System.runFinalizersOnExit方法以及執行緒中的suspend,stop,resume方法就是執行緒對立的,這些方法現在已經被廢棄。
私有鎖物件模式:
當一個類承諾了使用一個共有可訪問的所有物件時,就意味著允許客戶端以原子方式執行一個方法呼叫序列,若方法中使用了類物件作為公有可訪問鎖,對於一般的應用已經足夠了,但是併發集合使用的併發控制機制並不能與高效能的併發控制相相容,如果客戶端在超時還保持公有可訪問鎖不釋放,就會引起拒絕訪問攻擊(DDos),為了避免拒絕訪問攻擊,需要使用一個私有鎖物件:
private final Object lock = new Object();
public void test(){
synchronized(lock){
....
}
}
由於私有鎖物件不能被外部呼叫者所訪問,並且由於私有鎖物件是final的,保證鎖物件內容不會被修改,私有鎖模式可以避免子類和基類中同步方法相互干擾的問題,因此私有鎖物件模式特別適合於專門為基礎而設計的類以及無條件執行緒安全的類。
延遲初始化:
延遲初始化是延遲到需要域的值時才將它初始化的行為,如果永遠不需要這個值,則這個域就永遠不會被初始化。延遲初始化既適用於靜態域,也適用於例項域。
(1)正常初始的例項域:
//靜態域
private static final FieldType field1 = computeFieldValue();
//非靜態域
private static final FieldType field2 = computeFieldValue();
(2)使用同步方法延遲初始化:
private FieldType field;//也可以對靜態域延遲初始化private static FieldType field;
synchronized FiledType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
同步方法通常比非同步方法速度慢,因此使用同步方法的延遲初始化其實並不見得能提高效能。
(3)靜態域的Lazy initialization holder class模式:
延遲初始化持有類模式又叫按需初始化持有類模式,其本質是使用一個靜態內部類來持有要延遲初始化域引用,例子如下:
private static class FieldHolder{//靜態內部類
static final FieldType field = computerFieldValue();
}
static FieldType getField(){
return FieldHolder.field;
}
只有當getField方法第一次被呼叫時,FieldHolder.field引起靜態內部持有類FieldHolder初始化,在載入FieldHolder類的時候初始化其靜態域,由於是靜態域,因此只會被java虛擬機器在載入時初始化一次,並由虛擬機器保證執行緒安全。該模式的最大優勢在於避免使用同步方法,在保持延遲初始化的同時沒有增加額外的訪問開銷。
(4)例項域雙重檢查模式延遲初始化:
雙重檢查模式避免了在域被初始化之後訪問該域時鎖定開銷,其雙重檢查發生在:
第一次檢查:沒有鎖定,檢查域是否被初始化。
第二次檢查:鎖定,只有當第二次檢查時表明域沒有被初始化,才會對域進行初始化。
例子如下:
private volatile FieldType field;
FieldType getField(){
FieldType result = field;
if(result == null){//第一次檢查
synchronized(this){
result = field;
if(result == null){//第二次檢查
field = result = computeFieldValue();
}
}
}
return result;
}
雙重檢查中域宣告為volatile很重要,volatile關鍵字強制禁止java虛擬機器對指令亂序執行,在JDK1.5之前由於不同的java虛擬機器記憶體模型對volatile關鍵字實現支援不同,導致雙重檢查不能穩定正常執行,JDK1.5之後引入的記憶體模式解決了這個問題。
區域性變數result確保例項域field只在已經被初始化的情況下讀取一次,雖然不是嚴格要求,但是可以提升效能。雙重檢查模式將同步程式碼範圍縮小,減少了例項域訪問開銷。
注意沒有必須對靜態域使用雙重檢查模式,延遲初始化持有類模式更加優雅強大。
(5)例項域單重檢查模式:
如果延遲初始化的例項域可以接受重複初始化的例項域,則可以使用單重檢查模式來代替雙重檢查模式,例子如下:
private volatitle FieldType field;
FieldType getField(){
FieldType result = field;
if(result == null){//只檢查一次
field = result = computeFieldValue();
}
return result;
}
注意單重檢查模式中域例項變數依然要被宣告為volatile,由於可以接受重複初始化,因此去掉了第二次同步檢查,沒有同步的方法大大減少了訪問開銷。
雖然延遲初始化主要是一種優化,但它也可以用來打破類和例項初始化中的有害迴圈。和大多數的優化一樣,對於延遲初始化,最好的建議是“除非絕對必要,否則就不用延遲初始化”。延遲初始化是一把雙刃劍,它降低了初始化類或者建立例項的開銷,卻增加了訪問被延遲初始化的域的開銷,考慮到延遲初始化的域最終需要初始化的開銷以及域的訪問開銷,延遲初始化實際上降低了效能。