參考資料:
http://ifeve.com/java-memory-model-4/
http://www.infoq.com/cn/articles/java-memory-model-1
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
https://en.wikipedia.org/wiki/Singleton_pattern#Java_5_solution
https://www.ibm.com/developerworks/java/library/j-jtp06197/
1. volatile
final class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
以上代碼嘗試實現單例模式,但存在嚴重的線程安全風險。Java Memory Model定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。假設Thread1/Thread2並發,instance為它們的共享變量,Thread1與Thread2之間通信必須要經歷下面2個步驟:
- Thread1把本地內存更新過的instance刷新到主內存中去
- Thread2到主內存中去讀取Thread1之前已更新過的instance
那么可能的場景之一——Thread1執行完instance = new Singleton(),但刷新到主內存前Thread2的instance == null仍然成立,於是再次執行instance = new Singleton(),這時兩個線程得到了兩個不同的對象,與預期不符。
final class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
加入鎖和雙重校驗后,仍然存在風險,因為為了提高性能,編譯器和處理器常常會對指令做重排序,以Singleton instance = new Singleton()為例,它包含了三個指令:
- ①為instance分配內存
- ②調用Singleton構造方法
- ③把instance指向分配的內存地址
三個指令執行順序可能是①②③或①③②,在③執行之后,instance==null將不再成立。可能的場景——假設Thread1/Thread2並發,Thread1執行了除②以外的指令,Thread2的instance==null不成立,雖然得到了內存地址,但由於未調用構造方法而報錯。
final class Singleton { private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
為instance變量加上volatile關鍵字徹底解決問題。volatile的特性:
- volatile的變量修改后將立即刷新到主內存,其他線程即可讀取到新值
- 編譯器利用內存屏障的概念禁止上述三條指令的重排序,只允許①②③的執行順序
由於以上特性使volatile極適用於修飾多線程環境下的狀態標識。
2. ThreadLocal
當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
以非線程安全的SimpleDateFormat類為例,在並發運行時會出錯,但使用ThreadLocal維護則可以完美避免此問題
import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @Description: 測試ThreadLocal */ public class ThreadLocalTest { private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); private static final ThreadLocal<DateFormat> DATE_FORMAT = new ThreadLocal<DateFormat>() { public DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { String date = "2017-07-06"; testDateFormat(date); testThreadLocal(date); } private static void testDateFormat(String date) throws InterruptedException { multilpleThreadExecute(new Runnable() { @Override public void run() { try { System.out.println(df.parse(date)); } catch (ParseException e) { } } }); } private static void testThreadLocal(String date) throws InterruptedException { multilpleThreadExecute(new Runnable() { @Override public void run() { try { System.out.println(DATE_FORMAT.get().parse(date)); } catch (ParseException e) { } } }); } private static void multilpleThreadExecute(Runnable runnable) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { executorService.execute(runnable); } executorService.shutdown(); executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.DAYS); } }