饿汉式是相对于懒汉式来说的,懒汉式是第一次调用 getInstance() 方法时,才创建实例,而饿汉式则是不调用 getInstance() 方法,类初始化时,实例会被提前创建出来
// 1. 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public class TestSingleton { public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); } } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.ee.dd.mal.rest.Singleton1@782830e com.ee.dd.mal.rest.Singleton1@782830e
查看控制台输出:
com.ee.dd.mal.rest.Singleton1@782830e
com.ee.dd.mal.rest.Singleton1@782830e
说明是同一对象;
调用静态方法 otherMethod(),可以触发 Singleton1 的初始化操作,类初始化操作时,会调用构造方法,单例对象会被创建,getInstance() 时,拿到的是已经创建好的对象;
1.2单例的破坏方法 1.2.1反射破坏单例// 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); // 反射破坏单例 reflection(Singleton1.class); } private static void reflection(Class> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor> constructor = clazz.getDeclaredConstructor(); // 拿到一个无参的构造方法 constructor.setAccessible(true); // 设置私有的构造方法,也可以被使用 System.out.println("反射创建实例:" + constructor.newInstance()); // 调用构造方法的 newInstance() 也可以创建实例 } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.ww.mal.rest.Singleton1@782830e com.qq.ww.mal.rest.Singleton1@782830e private Singleton1() 反射创建实例:com.hbis.ttie.mal.rest.Singleton1@470e2030
可以看到,getInstance() 拿到一个对象,通过反射,构造方法的信息输出了出来,创建了另一个对象,一个类,两个对象,就不是单例了
如何预防?
修改代码
// 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); } System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); // 反射破坏单例 reflection(Singleton1.class); } private static void reflection(Class> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor> constructor = clazz.getDeclaredConstructor(); // 拿到一个无参的构造方法 constructor.setAccessible(true); // 设置私有的构造方法,也可以被使用 System.out.println("反射创建实例:" + constructor.newInstance()); // 调用构造方法的 newInstance() 也可以创建实例 } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton1@782830e com.qq.qq.mal.rest.Singleton1@782830e Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.qq.qq.mal.rest.TestSingleton.reflection(TestSingleton.java:25) at com.qq.qq.mal.rest.TestSingleton.main(TestSingleton.java:19) Caused by: java.lang.RuntimeException: 单例对象不能重复创建 at com.qq.qq.mal.rest.Singleton1.(Singleton1.java:11) ... 6 more
发现反射调用的时候,报错了
1.2.2反序列化破坏单例如果单例对象实现了 Serializable 接口,单例可以被破坏
// 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); } System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); // 反序列化破坏单例 serializable(Singleton1.getInstance()); } private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(instance); // 变成字节流 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); System.out.println("反序列化创建实例:" + ois.readObject()); // 把字节流还原成对象,这是一个新的对象,并且不调用构造方法 } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton1@782830e com.qq.qq.mal.rest.Singleton1@782830e 反序列化创建实例:com.hbis.ttie.mal.rest.Singleton1@2280cdac
查看控制台输出,创建了两个对象
如何预防?
// 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); } System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } // 方法名是固定的 // 在反序列化时,发现重写了 readResolve() ,那么会利用 readResolve() 的返回值,作为结果返回 public Object readResolve() { return INSTANCE; } }
public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); // 反序列化破坏单例 serializable(Singleton1.getInstance()); } private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(instance); // 变成字节流 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); System.out.println("反序列化创建实例:" + ois.readObject()); // 把字节流还原成对象,这是一个新的对象,并且不调用构造方法 } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton1@782830e com.qq.qq.mal.rest.Singleton1@782830e 反序列化创建实例:com.hbis.ttie.mal.rest.Singleton1@782830e
查看控制台输出,发现是同一对象
1.2.3Unsafe 破坏单例// 饿汉式 public class Singleton1 implements Serializable { // 构造私有 private Singleton1() { if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); } System.out.println("private Singleton1()"); } // 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1(); // 公共静态方法 public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } // 方法名是固定的 // 在反序列化时,发现重写了 readResolve() ,那么会利用 readResolve() 的返回值,作为结果返回 public Object readResolve() { return INSTANCE; } }
public static void main(String[] args) throws Exception { Singleton1.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton1.getInstance()); System.out.println(Singleton1.getInstance()); // Unsafe 破坏单例 unsafe(Singleton1.class); } // unsafe是jdk的内置类,不能直接访问, private static void unsafe(Class> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); // 根据类型,创建一个实例,也不会调用构造方法 System.out.println("Unsafe 创建实例:" + o); } 控制台输出: private Singleton1() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton1@782830e com.qq.qq.mal.rest.Singleton1@782830e Unsafe 创建实例:com.hbis.ttie.mal.rest.Singleton1@5fdef03a
查看控制台输出,发现创建了两个实例
还没找到预防方法
2.枚举饿汉式 2.1介绍enum Sex { MALE, FEMALE; }
光看代码不好理解,对其进行反编译,得到如下代码:
final class Sex extends Enum{ public static final Sex MALE; public static final Sex FEMALE; private Sex(String name, int ordinal) { super(name, ordinal); } static { MALE = new Sex("MALE", 0); FEMALE = new Sex("FEMALE", 1); $VALUES = values(); } private static final Sex[] $VALUES; private static Sex[] $values() { return new Sex[]{MALE, FEMALE}; } public static Sex[] values() { return $VALUES.clone(); } public static Sex valueOf(String value) { return Enum.valueOf(Sex.class, value); } }
final:修饰,说明不能被继承,不能有子类;
Enum
枚举类的饿汉式单例
// 枚举饿汉式 public enum Singleton2 { INSTANCE; // 以下代码,都是不必要的,为了测试方便而添加的 // 构造方法,默认是 private 的,去掉以后,依然是私有的 // idea也提示 Modifier 'private' is redundant for enum constructors 修饰符'private'对于enum构造函数是多余的 // 主要是为了打印信息,看构造方法是否调用 private Singleton2() { System.out.println("private Singleton2()"); } // 打印枚举类时,把 hash 码也打印出来,能区分是否是同一对象,默认打印枚举的名字 @Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } // 静态公共方法,获取单例 public static Singleton2 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton2.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton2.getInstance()); System.out.println(Singleton2.getInstance()); } 控制台输出: private Singleton2() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton2@782830e com.qq.qq.mal.rest.Singleton2@782830e
调用静态方法 otherMethod(),可以触发 Singleton2 的初始化操作,类初始化操作时,会调用构造方法,单例对象会被创建,getInstance() 时,拿到的是已经创建好的对象;
2.2破坏方法枚举饿汉式能天然防止反射(反射调用时会对枚举类型进行相应的检查)、反序列化(反序列化时会对枚举类进行特殊的处理)破坏单例
2.2.1Unsafe 破坏单例public enum Singleton2 { INSTANCE; // 以下代码,都是不必要的,为了测试方便而添加的 // 构造方法,默认是 private 的,去掉以后,依然是私有的 // idea也提示 Modifier 'private' is redundant for enum constructors 修饰符'private'对于enum构造函数是多余的 // 主要是为了打印信息,看构造方法是否调用 private Singleton2() { System.out.println("private Singleton2()"); } // 打印枚举类时,把 hash 码也打印出来,能区分是否是同一对象,默认打印枚举的名字 @Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } // 静态公共方法,获取单例 public static Singleton2 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton2.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); // 调用两次,看看是否是同一实例 System.out.println(Singleton2.getInstance()); System.out.println(Singleton2.getInstance()); // Unsafe 破坏单例 unsafe(Singleton2.class); } private static void unsafe(Class> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); System.out.println("Unsafe 创建实例:" + o); } 控制台输出: private Singleton2() otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> com.qq.qq.mal.rest.Singleton2@782830e com.qq.qq.mal.rest.Singleton2@782830e Unsafe 创建实例:com.hbis.ttie.mal.rest.Singleton2@5fdef03a
可以看到 Unsafe 创建了另一个对象
3.懒汉式单例 3.1介绍在使用时才会创建单例对象
// 懒汉式单例 public class Singleton3 implements Serializable { private Singleton3() { System.out.println("private Singleton3()"); } private static Singleton3 INSTANCE = null; public static Singleton3 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton3.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(Singleton3.getInstance()); System.out.println(Singleton3.getInstance()); } 控制台输出: otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> private Singleton3() com.qq.qq.mal.rest.Singleton3@782830e com.qq.qq.mal.rest.Singleton3@782830e
在调用 otherMethod() 时,未调用单例的构造方法,第一次调用 getInstance() 时,才会调用单例的构造方法,创建单例对象
3.2多线程环境运行懒汉式单例需要考虑是否运行在多线程环境下,需要考虑线程安全的问题
两个线程,同时调用 getInstance() 方法,线程1通过 if (INSTANCE == null) 校验,进入代码块,并未执行 INSTANCE = new Singleton3() 时,线程2也通过了 if (INSTANCE == null) 的校验,进入代码块,线程1、线程2都执行INSTANCE = new Singleton3(),创建了连个对象,就不再是单例对象了
如何解决?将代码改进一下
// 懒汉式单例 public class Singleton3 implements Serializable { private Singleton3() { System.out.println("private Singleton3()"); } private static Singleton3 INSTANCE = null; // Singleton3.class public static synchronized Singleton3 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
getInstance() 方法加 synchronized 关键字,对方法进行线程安全的保护
加在静态方法上的 synchronized,会给当前类(Singleton3.class)的 class 对象加一把锁,想进入方法,需要先获取锁,未释放锁前,其他线程无法进入
但是把 synchronized 加在整个方法上,虽然可以解决问题,但是性能上不好
查看代码发现,首次创建单例对象时,需要线程安全保护,单例对象创建后,就不存在线程安全问题了,因此,需求是首次创建单例对象时,有线程安全保护,后续的调用,无需线程安全保护
那么如何改进呢?
使用DCL懒汉式单例即双检锁懒汉式
4.DCL懒汉式单例 4.1介绍// 懒汉式单例 - DCL public class Singleton4 implements Serializable { private Singleton4() { System.out.println("private Singleton4()"); } private static volatile Singleton4 INSTANCE = null; // 可见性,有序性 public static Singleton4 getInstance() { if (INSTANCE == null) { synchronized (Singleton4.class) { if (INSTANCE == null) { INSTANCE = new Singleton4(); } } } return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
public static void main(String[] args) throws Exception { Singleton4.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(Singleton4.getInstance()); System.out.println(Singleton4.getInstance()); } 控制台输出: otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> private Singleton4() com.qq.qq.mal.rest.Singleton4@782830e com.qq.qq.mal.rest.Singleton4@782830e
代码的关键在于两次 if (INSTANCE == null) ,这也是双检索名字的来源
4.2为什么检查两次?假如没有内部的检查
public static Singleton4 getInstance() { if (INSTANCE == null) { synchronized (Singleton4.class) { INSTANCE = new Singleton4(); } } return INSTANCE; }
首次创建单例对象时,线程1、线程2同时进行,因为 INSTANCE = null,所以可以通过外部 if (INSTANCE == null) 校验,进入 if 代码块,这次线程2首先拿到了锁,然后创建单例对象,然后解锁返回,这时线程1获得锁,进入代码块,因为没有内部 if (INSTANCE == null) 校验,会再次创建一个单例对象,返回解锁返回,这就有问题了;
4.3为什么必须使用 volatile ?双检锁中,需要给静态变量使用 volatile 来修饰(volatile 可以解决共享变量的可见性,有序性问题),在双检锁中使用 volatile,是为了保证有序性
为何必须加 volatile?
`INSTANCE = new Singleton4()` 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值
CPU可能会对指令的执行次序进行优化(如果两指令之间,没有因果关系,就可能会被调换执行次序)
创建对象即分配内存空间,一定会在调用构造、给静态变量赋值前执行,调用构造方法是目的是为了给当前实例的成员变量进行初始化赋值,静态变量赋值也是一个赋值操作
其中后两步可能被指令重排序优化,变成先赋值、再调用构造,在单线程下,调换顺序没有问题,是一种优化手段,但是如果多线程环境下,就可能有问题了
如果线程1 ,执行 INSTANCE = new Singleton4() ,先执行了赋值,还未调用构造,此时线程2 执行到第一个 if (INSTANCE == null) 校验,发现 INSTANCE 已经不为 null,就会返回,但是构造方法还没有执行,返回的是一个未完整构造的对象
解决方法就是给共享变量加 volatile 修饰(大意是加了 volatile 修饰,会在赋值语句后,加一个内存屏障,也就说是不会出现调用构造方法时的赋值,越过内存屏障,出现在给静态变量赋值之后)
4.4为什么饿汉式单例使用 volatile ?饿汉式单例无需考虑多线程下对象创建的问题
// 静态成员变量 private static final Singleton1 INSTANCE = new Singleton1();
单例对象赋值给了静态成员变量,给静态变量赋值的操作,会放在这个类的静态代码块中执行,静态代码块中的线程安全有虚拟机来负责,我们无需考虑
5.懒汉式单例 - 内部类 5.1介绍将对象的创建,放入静态代码块中,那就意味着是线程安全的
// 懒汉式单例 - 内部类 public class Singleton5 implements Serializable { private Singleton5() { System.out.println("private Singleton5()"); } // 静态内部类 private static class Holder { static Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return Holder.INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
首先需要创建一个静态内部类,内部类可以访问外部类的私有变量,私有方法, 所以在内部类中创建了单例对象,并且赋值给了内部类的静态变量,刚才说了,静态变量的赋值是放在静态代码块中执行的,那么对象的创建(new Singleton5())就是线程安全的
在 getInstance() 方法中使用内部类访问它的变量,这是就会触发内部类的加载,链接,初始化,在初始化时,会创建单例对象,这样既有懒汉式的特性又能保证创建时的线程安全
public static void main(String[] args) throws Exception { Singleton5.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(Singleton5.getInstance()); System.out.println(Singleton5.getInstance()); } 控制台输出: otherMethod() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> private Singleton5() com.qq.qq.mal.rest.Singleton5@782830e com.qq.qq.mal.rest.Singleton5@782830e6.JDK 中哪些地方提现了单例模式?
* Runtime 体现了饿汉式单例
* Console 体现了双检锁懒汉式单例
* Collections 中的 EmptyNavigableSet 内部类懒汉式单例
* ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
* Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例