参考资料:
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); } }