单例模式第一版:
public class Singleton1 { private Singleton1(){}//私有化构造函数 private static Singleton1 instance = null;//单例对象 //静态工厂方法 public static Singleton1 getInstance(){ if(instance == null){ instance = new Singleton1(); } return instance; } }
如上单例模式却不是线程安全的。两个线程同时满足 if(instance == null){}就会new两次
改进:单例模式第二版:
public class Singleton1 { private Singleton1(){}//私有化构造函数 private static Singleton1 instance = null;//单例对象 //静态工厂方法 public static Singleton1 getInstance(){ if(instance == null){//双重检测机制 synchronized (Singleton1.class) { if(instance == null){//双重检测机制,防止A线程进来new了对象后释放锁,B线程获得所进来直接又new对象 instance = new Singleton1(); } } } return instance; } }
但是上述代码,也会存在线程安全问题。原因:可能会出现JVM指令重排
一般来说,对于instance = new Singleton1(),一般有三步,1.分配内存对象 2. 初始化对象 3. 将instance指向分配的内存地址
但是这些指令顺序不是一成不变的,有可能会经过JVM和CPU的优化,指令重排城下面的顺序。
1.分配内存对象 2. 将instance指向分配的内存地址 3. 初始化对象
当A执行完2时,被B抢占线程执行权时,B会判断 if(instance == null)发现不为null,则会返回未初始化的对象。
优化方案单例模式第三版:
public class Singleton3 { private Singleton3(){}//私有化构造函数 private volatile static Singleton3 instance = null;//单例对象 //静态工厂方法 public static Singleton3 getInstance(){ if(instance == null){//双重检测机制 synchronized (Singleton1.class) { if(instance == null){//双重检测机制 instance = new Singleton3(); } } } return instance; } }
加volatile关键字,volatile会禁用编译优化,并强制刷新缓存。对于instance = new Singleton1(),一定会是,1.分配内存对象 2. 初始化对象 3. 将instance指向分配的内存地址
加final关键字,禁用编译优化重排序
用静态内部类实现单例:
/** * * @author FL * 内部类只有当外部类被调用的时候才会加载 * 兼顾了饿汉式的内存浪费也兼顾了懒汉式1、2的问题 */ public class LazyThree { //private boolean intialized = false;//怎么防止反射暴力修改 // 默认使用的时候会初始化内部类,在线程之前被初始化,巧妙的避免了线程安全的问题 // 如果没使用的时候内部类是不加载的 private LazyThree() { //防止反射暴力访问 synchronized (LazyThree.class) { if (Sign.intialized == false) { Sign.intialized = true; } else { throw new RuntimeException("单例已经被侵犯"); } } } //static 使其可以被共享 //final 保证其不会被重写、重载 public static final LazyThree getInstance(){ return LazyHold.lazy; } public static class LazyHold{ private static final LazyThree lazy = new LazyThree(); } public static class Sign{ private static boolean intialized = false; } }
简化版:利用反射可以打破单例
public class Singleton4 { private Singleton4(){}//私有化构造函数 private static class LazyHolder{ private static final Singleton4 instance = new Singleton4(); } //静态工厂方法 public static Singleton4 getInstance(){ return LazyHolder.instance; } }
反射代码:
Class<?> clazz=LazyThree.class; Constructor<?> c = clazz.getDeclaredConstructor();//获取构造器 //反射暴力访问 c.setAccessible(true); Object O1 = c.newInstance(); Object O2 = c.newInstance(); System.out.println(O1); System.out.println(O2); /** * 通过反射已经拿到对象了,在通过类本身调静态方法获取对象时, * 就会抛出异常,单例被侵犯 */ Object O3=clazz.newInstance(); System.out.println(O3);
1.从外部无法访问静态内部类,Instance初始化的时机不是在LazyThree 被加载的时候,而是在调用LazyThree .getInstance()的时候LazyHold被加载的时候。这种实现方式利用了classLoader的加载机制来实现懒加载,并保证单里的线程安全。
用枚举实现单例模式:
public enum SingletonEnun { INSTANCE; }
SingletonEnun类被加载的时候就初始化了,属于饿汉式。Enum语法糖,JVM会阻止反射获取枚举的私有方法。
使用枚举实现的单例模式,不但可以防止反射强行构建单例对象,而且可以在枚举对象被反序列化时,保证反序列化的结果是同一对象。对于其他实现的单例模式,要想可序列化、反序列化为同一对象,则必须实现readResolve方法。
枚举解析:
定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。 通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。 而且,枚举中的各个枚举项同事通过static来定义的。如: public enum T { SPRING,SUMMER,AUTUMN,WINTER; } 反编译后代码为: public final class T extends Enum { //省略部分内容 public static final T SPRING; public static final T SUMMER; public static final T AUTUMN; public static final T WINTER; private static final T ENUM$VALUES[]; static { SPRING = new T("SPRING", 0); SUMMER = new T("SUMMER", 1); AUTUMN = new T("AUTUMN", 2); WINTER = new T("WINTER", 3); ENUM$VALUES = (new T[] { SPRING, SUMMER, AUTUMN, WINTER }); } } 了解JVM的类加载机制应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)中介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。 也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。 所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。