標籤:desc 重新整理 static single ide 抽象 java多線程 throws 參考
參考資料:
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); }}
Java多線程系列八——volatile和ThreadLocal