單例模式應該是設計模式中應用最廣泛的模式旋奢,通過單例模式迄薄,保證在系統(tǒng)的生命周期中只有一個在運行恳邀,在java中懦冰,單面經常寫成如下格式。
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
看起來很漂亮的代碼谣沸,但是如果有在多線程的環(huán)境下呢?線程1執(zhí)行完了if判斷后被中斷笋颤, 線程2開始執(zhí)行并生成了一個實例乳附,線程1接著執(zhí)行,又生成了一個伴澄。最簡單的方法是加鎖赋除。代碼如下:
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static synchronized Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
這樣寫,這個代碼執(zhí)行沒有問題非凌,但是想一想举农,單例模式只生成一個實例,所以實際上只有在第一次的時候if語句條件成立敞嗡,在后面無論執(zhí)行多少次颁糟,都會直接返回靜態(tài)實例,但是每次都要鎖整段代碼喉悴,性能和效率上是一種浪費棱貌。那么怎么既能保證安全,又能保證性能呢箕肃,有好幾種方法婚脱,這里先說比較常用的雙檢測(double-check)機制。
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static Singleton getInstance() {
If (instance == null) {
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這樣在第一次if條件成立的時候勺像,即使線程被中斷障贸,因為有鎖保護,在生成實例的過程中被保護吟宦,被中斷的線程或者新的線程在這個過程中不能執(zhí)行篮洁,而等鎖推出后,其他線程恢復執(zhí)行的時候督函,又要做一次判斷嘀粱,所以不會出現(xiàn)重復生成激挪。而實例生成后,每次只需要執(zhí)行第一個if判斷锋叨,就直接返回垄分,所以也不影響代碼效率⊥藁牵看起來很完美薄湿,可以還沒完,這個代碼還有一個bug偷卧,就是java的內存模型的問題被疏忽了【第一個問題豺瘤,java的內存模型】。
在java多線程的時候听诸,一般每個線程都會有一個工作內存(對CPU的高速緩存機制的抽象)坐求,如上圖所示的工作內存和主內存之間的關系及操作順序。線程運行的時候是使用和修改自己的工作內存中的變量晌梨,而并不會立即回寫到主內存桥嗤,所以如果上面的代碼,如果線程1執(zhí)行過程中被打斷仔蝌,而線程2執(zhí)行完成泛领,并生成一個實例,但是由于線程1還是使用的是自己工作內存的值敛惊,那么還會出現(xiàn)多次生成實例的問題渊鞋。所以我們只考慮了原子性,而疏忽了多線程變成的可見性和有序性瞧挤。所以可以修改一行代碼:
private static volatile Singleton instance;
這個差別就是用volatile修飾instance變量锡宋,volatile聲明的變量解決可見性和有序性,對于可見行:線程中每次use變量時皿伺,都需要連續(xù)執(zhí)行read->load->use幾項操作员辩,即所謂的每次使用都要從主內存更新變量值,這樣其它線程的修改對該線程就是可見的鸵鸥。并且奠滑,線程每次assign變量時,都需要連續(xù)執(zhí)行assign->store->write幾項操作妒穴,即所謂每次更新完后都會回寫到主內存宋税,這樣使得其它線程讀到的都是最新數據。對于有序性讼油,對于volatile變量前面的代碼的修改不會被優(yōu)化到volatile變量后面杰赛,來避免虛擬機的優(yōu)化造成代碼的執(zhí)行順序的變化。
// Thread 1
Worker worker = new Worker;
Boolean initialed = true;
//Thread 2
While(! Initialed){
sleep(100);
}
worker.dosomething();
如果不用volatile生命initialed變量矮台,假如虛擬機對線程1的代碼做了重排乏屯,線程2執(zhí)行過程中就會出異常(雖然概率可能會非常低)根时。
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
最后再看后面這段代碼,運行結果是10000嗎辰晕?實際上這段程序并不能保證結果是10000蛤迎,因為volatile只能保證變量的可見性,而不能保證變量的原子性含友,而在java中替裆,只有賦值和基本類型變量的讀取能保證原子性。假如線程1剛從主內存read了變量后被中斷窘问,另外一個線程開始執(zhí)行辆童,讀取了主內存的變量,做了自加操作惠赫,等線程1開始執(zhí)行把鉴,還是用剛才讀取的主內存的值,因為可見性只是保證每次從主內存去讀值儿咱。
編碼是個苦力纸镊,每一行代碼都得仔細思考,怎么讓coder有動力去思考概疆,有時間去思考?