1.饿汉式
1.1介绍
饿汉式是相对于懒汉式来说的,懒汉式是第一次调用 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.<init>(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<Sex> {
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<Sex>:枚举父类,不能在代码中直接写,是编译不通过的,继承关系是编译器在编译时加上的;
枚举类的饿汉式单例
// 枚举饿汉式
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@782830e
6.JDK 中哪些地方提现了单例模式?
* Runtime 体现了饿汉式单例
* Console 体现了双检锁懒汉式单例
* Collections 中的 EmptyNavigableSet 内部类懒汉式单例
* ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
* Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例