Java 实现单例设计模式的部分例子,并不是全部
普通饿汉式 何为饿汉,就是很饿了,要马上能吃到东西(实例先初始化好,要用时就直接拿来用)
1 2 3 4 5 6 7 8 9 10 11 12 package singleton;public class Singleton { private static final Singleton INSTANCE = new Singleton (); private Singleton () { } public static Singleton getInstance () { return INSTANCE; } }
这种方式就能实现单例了吗?测试下
1 2 3 4 public static void main (String[] args) { System.out.println(Singleton.getInstance()); System.out.println(Singleton.getInstance()); }
输出相同的地址,确实能实现
1 2 singleton.Singleton@6bf2d08e singleton.Singleton@6bf2d08e
从上面输出可以看到是实现了的,但是有潜在的问题
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Exception { System.out.println(Singleton.getInstance()); Constructor<?>[] declaredConstructors = Singleton.class.getDeclaredConstructors(); for (Constructor<?> declaredConstructor : declaredConstructors) { declaredConstructor.setAccessible(true ); Object obj = declaredConstructor.newInstance(); System.out.println(obj); } }
这样就会得到两个不同的对象
1 2 singleton.Singleton@6bf2d08e singleton.Singleton@1b701da1
如何解决?
要实现单例,核心有两点:
私有化构造方法,不能让别人 new 提供获取实例的方法 上面的代码就是把原来 private 的构造方法设为 public,然后 new 了下,那知道了这一点,就可以在构造方法那里对应地进行处理
我都饿汉了,实例早就有了,你还来 new,抛个异常给你
1 2 3 4 5 private Singleton () { if (INSTANCE != null ) { throw new RuntimeException ("!" ); } }
再来测试下
1 2 singleton.Singleton@6bf2d08e Exception in thread "main" java.lang.reflect.InvocationTargetException
再已经有一个实例的情况下,再通过反射来 new,就会得到一个异常
但是,没有其他的问题了吗?再来测试下
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Exception { System.out.println(Singleton.getInstance()); ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(Singleton.getInstance()); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bos.toByteArray())); Object obj = ois.readObject(); System.out.println(obj); }
涉及到了流的操作,单例类需要实现 Serializable
接口
结果如下
1 2 singleton.Singleton@5eb5c224 singleton.Singleton@5ae9a829
又得到了两个不同的实例
如何解决?
1 2 3 private Object readResolve () { return INSTANCE; }
再次查看输出结果
1 2 singleton.Singleton@5eb5c224 singleton.Singleton@5eb5c224
解决了,但是为什么 readResolve() 能解决序列化导致两个对象的问题,这个就要稍微看点 JDK 源码(JDK 1.8)
首先从测试代码的 readObject() 入手(删掉不重点关注或者说看不懂的代码,看源码真的要深入每一行吗?你要这么说,那最核心的都是 native 方法)
1 Object obj = ois.readObject();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class: java.io.ObjectInputStream public final Object readObject () throws IOException, ClassNotFoundException { return readObject(Object.class); } private final Object readObject (Class<?> type) throws IOException, ClassNotFoundException { ... Object obj = readObject0(type, false ); return obj; ... } private Object readObject0 (Class<?> type, boolean unshared) throws IOException { ... case TC_OBJECT: if (type == String.class) { throw new ClassCastException ("Cannot cast an object to java.lang.String" ); } return checkResolve(readOrdinaryObject(unshared)); ... } private Object readOrdinaryObject (boolean unshared) throws IOException { if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { if (rep != null ) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1 ); } } handles.setObject(passHandle, obj = rep); } } return obj; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class: java.io.ObjectStreamClass boolean hasReadResolveMethod () { requireInitialized(); return (readResolveMethod != null ); } private ObjectStreamClass (final Class<?> cl) { ... if (serializable) { AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { readResolveMethod = getInheritableMethod( cl, "readResolve" , null , Object.class); return null ; } }); } ... }
上面就是为什么 readResolve() 能防止反序列化出来不同的对象
但是这样,就能实现单例了吗?再来测试下别的
1 2 3 4 5 6 7 8 public static void main (String[] args) throws Exception { System.out.println(Singleton.getInstance()); Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); System.out.println(unsafe.allocateInstance(Singleton.class)); }
1 2 singleton.Singleton@2a139a55 singleton.Singleton@6d06d69c
这样又能得到两个对象了,因为是比较底层的 Unsafe 类,所以预防不了
那么有没有其他方法,有,就是枚举饿汉
枚举类饿汉式 1 2 3 public enum Singleton1 { INSTANCE; }
测试反射破坏单例
1 2 3 4 5 6 7 8 public static void main (String[] args) throws Exception { System.out.println(Singleton.INSTANCE); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true ); Object obj = constructor.newInstance(); System.out.println(obj); }
得到了一个异常 java.lang.NoSuchMethodException,通过 javap 编译出来的 class 文件,可以看到类的结构
1 2 3 4 5 6 public final class singleton .Singleton extends java .lang.Enum<singleton.Singleton> { public static final singleton.Singleton INSTANCE; public static singleton.Singleton[] values(); public static singleton.Singleton valueOf (java.lang.String) ; static {}; }
里面确实没有无参构造方法,再看继承的 java.lang.Enum 类
1 2 3 4 5 6 7 8 9 public abstract class Enum <E extends Enum <E>> implements Comparable <E>, Serializable { ... protected Enum (String name, int ordinal) { this .name = name; this .ordinal = ordinal; } ... }
protected 能被继承,所以枚举类里是有带参的构造方法的,如果调用带参的构造方法,会发生什么
1 2 3 4 5 6 7 8 public static void main (String[] args) throws Exception { System.out.println(Singleton.INSTANCE); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); Object obj = constructor.newInstance(); System.out.println(obj); }
1 2 INSTANCE Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
可以调用,但是报错了,看回反射的源码,可以看到里面做了一个判断
1 2 3 4 5 6 7 8 9 10 11 12 class: java.lang.reflect.Constructor @CallerSensitive public T newInstance (Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { ... if ((clazz.getModifiers() & Modifier.ENUM) != 0 ) throw new IllegalArgumentException ("Cannot reflectively create enum objects" ); ... }
所以枚举饿汉,是在 JDK 的层面进行了防反射
再试试反序列化
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Exception { System.out.println(Singleton.INSTANCE); ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(Singleton.INSTANCE); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bos.toByteArray())); Object obj = ois.readObject(); System.out.println(obj); }
得到的是相同的对象,要了解原因,需要从序列化看起
1 oos.writeObject(Singleton.INSTANCE);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class: java.io.ObjectOutputStream public final void writeObject (Object obj) throws IOException { ... try { writeObject0(obj, false ); } catch (IOException ex) { if (depth == 0 ) { writeFatalException(ex); } throw ex; } } private void writeObject0 (Object obj, boolean unshared) throws IOException { ... else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } ... } private void writeEnum (Enum<?> en, ObjectStreamClass desc, boolean unshared) throws IOException { ... writeString(en.name(), false ); }
再看反序列化,大部分代码和上面普通类的反序列化相同,这里重点列出不同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class: java.io.ObjectInputStream private Object readObject0 (Class<?> type, boolean unshared) throws IOException { ... case TC_ENUM: if (type == String.class) { throw new ClassCastException ("Cannot cast an enum to java.lang.String" ); } return checkResolve(readEnum(unshared)); ... } private Enum<?> readEnum(boolean unshared) throws IOException { ... Enum<?> result = null ; ... Enum<?> en = Enum.valueOf((Class)cl, name); result = en; ... return result; } public static <T extends Enum <T>> T valueOf (Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null ) return result; if (name == null ) throw new NullPointerException ("Name is null" ); throw new IllegalArgumentException ( "No enum constant " + enumType.getCanonicalName() + "." + name); }
所以枚举饿汉也是在 JDK 的层面进行了防反序列化
那么 Unsafe 又怎么样
1 2 3 4 5 6 7 8 public static void main (String[] args) throws Exception { System.out.println(Singleton.INSTANCE); Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); System.out.println(unsafe.allocateInstance(Singleton.class)); }
返回了一个 null,因为这个是 native 方法,具体实现的代码看不懂,这里只放出 Java 这边的代码
1 2 3 4 5 6 class: sun.misc.Unsafe public native Object allocateInstance (Class<?> cls) throws InstantiationException;
所以也可以说,枚举饿汉也能防 Unsafe
普通懒汉式 懒汉就是没有这么饿,但是很懒,因为懒,所以我不提前给你准备好东西,你催我的时候我再来准备,下面的代码都能实现单例,结果就不放了
1 2 3 4 5 6 7 8 9 10 public class Singleton { private static Singleton INSTANCE = null ; public static Singleton getInstance () { if (INSTANCE == null ) { INSTANCE = new Singleton (); } return INSTANCE; } }
这种方法有个问题,因为 INSTANCE 其实是一个共享变量,共享变量在多线程的环境下就会有资源竞争的问题,所以可能两个线程都会判断 INSTANCE 是 null,所以创建了两个对象
既然这样,那无脑 synchronized 就完了
1 2 3 4 5 6 7 8 9 10 public class Singleton { private static Singleton INSTANCE = null ; public synchronized static Singleton getInstance () { if (INSTANCE == null ) { INSTANCE = new Singleton (); } return INSTANCE; } }
可以是可以,但是锁的粒度太大了,并发量很大的情况下性能会有影响(大部分场景下这些都是空话套话),所以要将锁细化
1 2 3 4 5 6 7 8 9 10 11 12 public class Singleton { private static Singleton INSTANCE = null ; public static Singleton getInstance () { if (INSTANCE == null ) { synchronized (Singleton.class) { INSTANCE = new Singleton (); } } return INSTANCE; } }
这样一来,只有 INSTANCE 是 null 的时候才会加锁,性能就会大大提升
但是也会有一个问题,理想情况下是请求一个接着一个来,第一个来了,null,初始化,后面的直接 return,但是在多线程环境下,也会存在两个线程都判断是 null,所以都进去了 if 代码块,虽然只有一个线程能进入 synchronized 代码块,但是一个线程 new 了,就释放锁了,第二个线程还是会 new,这样就会有两个实例
如何解决?在 synchronized 块里再判断一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton { private static Singleton INSTANCE = null ; public static Singleton getInstance () { if (INSTANCE == null ) { synchronized (Singleton.class) { if (INSTANCE == null ) { INSTANCE = new Singleton (); } } } return INSTANCE; } }
这样虽然有多个线程判断 INSTANCE 是 null,从而进入 if 代码块,但是只能有一个线程能进入 synchronized 块进行 new 对象,虽然 new 完了别的线程还能进入,但进入后再判断,就不会再 new 了
看似解决了,但是这种方式在 JVM 层面有一个问题
new 对象的时候一般会包括但不限于以下几个步骤:
但是 JVM 会进行一些优化,江湖人称指令重排序,就是可能会先赋值了再初始化变量,这样以来在多线程的环境下就会得到一个未初始化的对象,那用起来会就有奇怪的问题,为了禁止这个负优化(单例场景下是负优化),需要用 volatile 关键字
volatile 通过增加内存屏障的方式,禁止指令重排序,除此之外,还可以手动增加内存屏障,通过 Unsafe 类的一些 fence 相关的方法(这个不常用,除非是很底层的代码),volatile 还保证可见性,详细的东西可能单独谈比较合适
其他注意的点和饿汉式相同
内部类懒汉式 内部类是类加载的时候就初始化,类加载是线程安全的,其他注意的点也和上面的类似
1 2 3 4 5 6 7 8 9 10 11 12 public class Singleton { private Singleton () { } private static final class InstanceHolder { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return InstanceHolder.INSTANCE; } }
最后总结:大家都说用枚举类单例好