1、饿汉
1.1、饿汉式(静态常量,线程安全)
public class HungrySingle { private final static HungrySingle instance = new HungrySingle(); private HungrySingle() { System.out.println(Thread.currentThread().getName()); } public static HungrySingle getInstance() { return instance; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread((()-> { HungrySingle.getInstance(); })).start(); } } }
输出
Thread-0
步骤:
- 构造器私有
- 创建内部对象
- 写一个静态公共方法
优点:
- 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:
- 在类装载的时候就完成实例化,没有达到Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果
这种单例模式可用,可能造成内存浪费。
1.2、饿汉式(静态代码块,线程安全)
public class HungrySingle { private final static HungrySingle instance; static { instance = new HungrySingle(); } private HungrySingle() { System.out.println(Thread.currentThread().getName()); } public static HungrySingle getInstance() { return instance; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread((()-> { HungrySingle.getInstance(); })).start(); } } }
输出
Thread-100
步骤:
- 构造器私有
- 创建内部对象
- 静态代码块实例化内部对象
- 写一个静态公共方法
优缺点同饿汉(静态常量)
2、懒汉
2.1、懒汉式(普通,线程不安全)
public class UnsafeLazySingle { private static UnsafeLazySingle instance; private UnsafeLazySingle() { System.out.println(Thread.currentThread().getName()); } public static UnsafeLazySingle getInstance() { if (null == instance) { instance = new UnsafeLazySingle(); } return instance; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread((()-> { UnsafeLazySingle.getInstance(); })).start(); } } }
输出
Thread-0 Thread-3 Thread-2 Thread-1
优点:
- 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
缺点:
- 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
多线程不能用
2.2、懒汉式(同步方法,线程安全)
class LazySingleF{ private static LazySingleF instance; private LazySingleF(){ System.out.println(Thread.currentThread().getName()); } public static synchronized LazySingleF getInstance() { if (instance == null) { instance = new LazySingleF(); } return instance; } }
输出
Thread-0
优点:
- 解决了线程安全问题
缺点:
- 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。
在实际开发中,不推荐使用这种方式
2.3、懒汉式(同步代码块,线程不安全)
class LazySingleB{ private static LazySingleB instance; private LazySingleB() { System.out.println(Thread.currentThread().getName()); } public static LazySingleB getInstance() { if (instance == null) { synchronized (LazySingleB.class) { instance = new LazySingleB(); } } return instance; } }
输出
Thread-4 Thread-6 Thread-1 Thread-5 Thread-2 Thread-0 Thread-3
缺点:
- 不能线程同步。假如一个线程进入了if (instance == null),还未来得及执行,另一个线程也通过了这个判断语句,会产生多个实例
- 效率很低
多线程不能使用
2.4、懒汉式(双重检查,线程安全)
Double Check Lock(DCL)
class LazySingleD{ private volatile static LazySingleD instance; private LazySingleD() { System.out.println(Thread.currentThread().getName()); } public static LazySingleD getInstance() { if (null == instance) { synchronized (LazySingleD.class) { if (instance ==null) { instance = new LazySingleD(); } } } return instance; } }
输出
Thread-0
Double-Check Lock概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
注意:
由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下:
INSTANCE = new SingleTon();
这个步骤,其实在jvm里面的执行分为三步:
- 在堆内存开辟内存空间。
- 在堆内存中实例化SingleTon里面的各个参数。
- 把对象指向堆内存空间。
由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。
不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为
private volatile static LazySingleD instance;
就可解决DCL失效问题。volatile的内存栅栏功能,告知编译器的在标记的变量前后不使用优化功能。
优点:
线程安全;延迟加载;效率较高
在实际开发中,推荐使用这种单例设计模式
3、内部静态类
3.1、具体实现
class InnerClass { private InnerClass() { System.out.println(Thread.currentThread().getName()); } private static class InnerClassHolder { private static InnerClass instance = new InnerClass(); } public static InnerClass getInstance() { return InnerClassHolder.instance; } }
输出
Thread-0
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
即当InnerClass第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
3.2、类加载
JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:
- new一个关键字或者一个实例化对象时
- 读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)
- 调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上是类的主动引用。除此之外,所有引用类都不会对类进行初始化,称为被动引用。
静态内部类就属于被动引用的行列。
为什么?
我们再回头看下getInstance()方法,调用的是InnerClassHolder.instance,取的是InnerClassHolder里的instance对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个instance对象,而不用去重新创建。当getInstance()方法被调用时,InnerClassHolder才在InnerClass的运行时常量池里,把符号引用替换为直接引用,这时静态对象instance也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。
那么instance在创建过程中又是如何保证线程安全的呢?
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。
可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。
4、枚举
enum Enums { /** * 实例对象 */ INSTANCE; }
输出:
并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。
5、破坏单例模式的方法及解决办法
5.1、反射
除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例
public static void reflect() { try { HungrySingleC singleC1 = HungrySingleC.getInstance(); Constructor<HungrySingleC> constructor = HungrySingleC.class.getDeclaredConstructor(); constructor.setAccessible(true); HungrySingleC singleC2 = constructor.newInstance(); System.out.println(singleC1.hashCode() == singleC2.hashCode()); } catch (Throwable e) { e.printStackTrace(); } }
输出
false
解决办法:
private HungrySingleC() { if (instance == null) { throw new RuntimeException("已存在"); } System.out.println(Thread.currentThread().getName()); }
反射试图破坏枚举:
public static void reflect() { try { Enums e1 = Enums.INSTANCE; Constructor<Enums> constructor = Enums.class.getDeclaredConstructor(); constructor.setAccessible(true); Enums e2 = constructor.newInstance(); System.out.println(e1.hashCode() == e2.hashCode()); } catch (Throwable e) { e.printStackTrace(); } }
输出
java.lang.NoSuchMethodException: cn.edu..pattern.single.Enums.<init>() at java.base/java.lang.Class.getConstructor0(Class.java:3355) at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2559) at cn.edu..pattern.single.EnumSingle.reflect(EnumSingle.java:19) at cn.edu..pattern.single.EnumSingle.main(EnumSingle.java:35)
源码
public abstract class Enum<E extends Enum<E>> implements Constable, Comparable<E>, Serializable { private final String name; private final int ordinal; protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } }
枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:
public static void reflect() { try { Enums e1 = Enums.INSTANCE; Constructor<Enums> constructor = Enums.class.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); Enums e2 = constructor.newInstance(); System.out.println(e1.hashCode() == e2.hashCode()); } catch (Throwable e) { e.printStackTrace(); } }
输出
java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481) at cn.edu..pattern.single.EnumSingle.reflect(EnumSingle.java:21) at cn.edu..pattern.single.EnumSingle.main(EnumSingle.java:35)
Constructor类的newInstance方法源码:
// to ensure Reflection.getCallerClass optimization public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<?> caller = override ? null : Reflection.getCallerClass(); return newInstanceWithCaller(initargs, !override, caller); } /* package-private */ T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller) throws InstantiationException, IllegalAccessException, InvocationTargetException { if (checkAccess) checkAccess(caller, clazz, clazz, modifiers); if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } "unchecked") ( T inst = (T) ca.newInstance(args); return inst; }
反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
5.2、序列化
如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。
public static void serializable() { try { LazySingleD d = LazySingleD.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("LazySingleD.obj")); oos.writeObject(d); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("LazySingleD.obj"); ObjectInputStream ois = new ObjectInputStream(fis); LazySingleD d2 = (LazySingleD) ois.readObject(); ois.close(); System.out.println(d.hashCode() == d2.hashCode()); } catch (Throwable e) { e.printStackTrace(); } }
输出
false
序列化试图破坏枚举
public static void serializable() { try { EnumSerializable e = EnumSerializable.INSTANCE; ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSerializable.obj")); oos.writeObject(e); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSerializable.obj"); ObjectInputStream ois = new ObjectInputStream(fis); EnumSerializable e2 = (EnumSerializable) ois.readObject(); ois.close(); System.out.println(e.hashCode() == e2.hashCode()); } catch (Throwable e) { e.printStackTrace(); } }
输出
true
在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。