Java多線程系列八——volatile和ThreadLocal


參考資料:

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);
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM