背景:我們在實現單例模式的時候往往會忽略掉多線程的情況,就是寫的代碼在單線程的情況下是沒問題的,但是一碰到多個線程的時候,由於代碼沒寫好,就會引發很多問題,而且這些問題都是很隱蔽和很難排查的。
例子1:沒有volatile修飾的uniqueInstance
public class Singleton { private static Singleton uniqueInstance; private Singleton(){ } public static Singleton getInstance(){ if(uniqueInstance == null){ //#1 synchronized(Singleton.class){ //#2 if(uniqueInstance == null){ //#3 uniqueInstance = new Singleton(); //#4 System.out.println(Thread.currentThread().getName() + ": uniqueInstance is initalized..."); //#5.1 } else { System.out.println(Thread.currentThread().getName() + ": uniqueInstance is not null now..."); //#5.2 } } } return uniqueInstance; } }
1 public class TestSingleton { 2 public static void main(final String[] args) throws InterruptedException { 3 for (int i = 1; i <= 100000; i++) { 4 final Thread t1 = new Thread(new ThreadSingleton()); 5 t1.setName("thread" + i); 6 t1.start(); 7 } 8 } 9 10 public static class ThreadSingleton implements Runnable { 11 @Override 12 public void run() { 13 Singleton.getInstance(); 14 } 15 } 16 }
這里面的結果有可能會是:(沒有真正重現過,太難模擬了)
1 thread2: uniqueInstance is initalized... 2 thread3: uniqueInstance is initalized...
Singleton被實例化兩次了,和我們的單例模式設計期望值不一致:類永遠只被實例化一次.
原因分析:
1. thread2進入#1, 這時子線程的uniqueInstance都是為空的,thread2讓出CPU資源給thread3
2. thread3進入#1, 這時子線程的uniqueInstance都是為空的, thread3讓出CPO資源給thread2
3. thread2會依次執行#2,#3,#4, #5.1,最終在thread2里面實例化了uniqueInstance。thread2執行完畢讓出CPO資源給thread3
4. thread3接着#1跑下去,跑到#3的時候,由於#1里面拿到的uniqueInstance還是空(並沒有及時從thread2里面拿到最新的),所以thread3仍然會執行#4,#5.1
5. 最后在thread2和thread3都實例化了uniqueInstance
例子2:用volatile修飾的uniqueInstance
這里就不貼重復的代碼了,因為只是加多一個volatile來修飾成員變量:uniqueInstance,
但是結果卻是正確的了, 其中一個可能結果:
thread2: uniqueInstance is initalized
thread3: uniqueInstance is not null now...
原因分析:
volatile(java5):可以保證多線程下的可見性;
讀volatile:每當子線程某一語句要用到volatile變量時,都會從主線程重新拷貝一份,這樣就保證子線程的會跟主線程的一致。
寫volatile: 每當子線程某一語句要寫volatile變量時,都會在讀完后同步到主線程去,這樣就保證主線程的變量及時更新。
1. thread2進入#1, 這時子線程的uniqueInstance都是為空的(java內存模型會從主線程拷貝一份uniqueInstance=null到子線程thread2),thread2讓出CPU資源給thread3
2. thread3進入#1, 這時子線程的uniqueInstance都是為空的(java內存模型會從主線程拷貝一份uniqueInstance=null到子線程thread2), thread3讓出CPO資源給thread2
3. thread2會依次執行#2,#3,#4, #5.1,最終在thread2里面實例化了uniqueInstance(由於是volatile修飾的變量,會馬上同步到主線程的變量去)。thread2執行完畢讓出CPO資源給thread3
4. thread3接着#1跑下去,跑到#3的時候,會又一次從主線程拷貝一份uniqueInstance!=null回來,所以thread3就直接跑到了#5.2
5. 最后在thread3不再會重復實例化uniqueInstance了
參考文章:如何在Java中使用雙重檢查鎖實現單例