0x1、定义
网络异常,图片无法展示
|
0x2、单例写法的演进
① 饿汉式(没有懒加载,线程安全,常用)
public class Singleton () { private static Singleton instance = new Singleton() private Singleton(){ } public static Singleton getInstance() { return instance; } }
- 优点:类装载(ClassLoader)时就完成实例化,避免线程同步问题,没加锁,执行效率高;
- 缺点:没有懒加载,即使没用到这个实例还是会加载;
② 懒汉式(懒加载,线程不安全,不推荐使用)
就是在饿汉式的基础上加了一个判空,调用getInstance()方法才初始化实例:
public class Singleton { private static Singleton instance = null; private Singleton() { } private static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
虽然实现了懒加载,却存在线程安全问题,比如两个线程,都刚好走到判空,实例为空初始化,结果可能导致实例化了两个Singleton对象,破坏了单例,一种升级版的解决方式是加锁。
③ 升级版懒汉式(线程安全,但效率低,不推荐使用)
public class Singleton { private Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
给getInstance()函数加锁,保证了线程安全,但也导致了函数的并发度很低,相当于串行操作,频繁调用此函数,会频繁地加锁、释放锁、效率太低。
而且,其实只需要在new的时候考虑线程同步就行了,所以改进后的DCL单例来了~
④ 懒汉式双重校验锁(DCL,线程安全,推荐使用)
public 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; } }
代码看似完美,但还存在最后一个问题:指令重排序
JVM在保证最终结果正确的情况下,可以不按照编码的顺序执行语句,尽可能地提高程序的性能。
创建一个对象,在JVM中会经过这三步:
- 1、为instance分配内存空间;
- 2、初始化instance对象;
- 3、将instance指向分配好的内存空间;
在这三步中,第2、3步有可能发生指令重排现象,导致对象的创建顺序变成了:1-3-2,多个线程在获取对象时,有可能获取到为初始化的instance对象对象,引起NPE异常。示例流程图如下所示:
网络异常,图片无法展示
|
而使用volatile关键字修饰变量,可以防止指令重排序(原理是内存屏障),使得指令执行顺序与程序指明顺序一致。
修改后的代码如下:
public 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; } }
上面这个防止指令重排序又称**有序性
,接着说说可见性
**,即 每一时刻线程读取该变量的值都是内存中最新的值。
也可以这样理解:
volatile修饰的变量,在线程对其进行写入操作时,不会把值缓存到工作内存中,而是直接将修改后的值重新刷回主内存。而当处理器监控(嗅探)到其他线程中该变量在主内存中的内存地址发生变化时,会让这些线程重新到主内存中拷贝这个变量的最新值到工作内存中,而不是继续使用工作内存中的旧缓存。
未加volatile的简单代码示例如下:
public class JavaTest { public static void main(String[] args) { Test test = new Test(); test.start(); while (true){ if (test.isFlag()) { System.out.println("flag为true"); break; } } } } class Test extends Thread { private boolean flag = false; public boolean isFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag = " + flag); } }