小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
单例模式概念
1.什么是单例模式?
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
2.实现单例模式的思路
1. 构造私有:
如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。
2.以静态方法返回实例:
因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。
3.确保对象实例只有一个:
只对类进行一次实例化,以后都直接获取第一次实例化的对象。
1.饿汉式
/**
* 单例模式
* 饿汉式
*/
public class Singleton1 implements Serializable {
// 构造函数
private Singleton1() {
System.out.println("开始初始化实例======");
}
private final static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
// 一个其他方法用来测试执行该方法时会不会实例化
public static void otherMethod() {
System.out.println("执行其他方法======");
}
}
测试饿汉式执行代码,并且通过反射,反序列化,和unsafe类,测试是否可以破坏单例模式
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());
reflection(Singleton1.class);
serializable(Singleton1.getInstance());
unsafe(Singleton1.class);
}
//通过反射破坏单例模式
public static void reflection(Class<?> clazz) throws Exception {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("反射创建实例:"+constructor.newInstance());
}
//通过反序列化破坏实例
public static void serializable(Object instance) throws Exception {
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());
}
//通过unsafe破坏单例
public static void unsafe(Class<?> clazz) throws InstantiationException {
Object instance = UnsafeUtils.getUnsafe().allocateInstance(clazz);
System.out.println("unsafe 创建实例:"+instance);
}
}
执行结果如下:
可以看到确实在执行其他方法前,就初始化了该实例,并且也两次调用 getInstance() 方法也成功返回来同一实例,但是可以看到通过反射,反序列化,和unsafe类都可以破坏该单例模式,创建了不同的实例对象.
优化饿汉式的代码,可以解决反射,反序列化的破坏,但是目前没有找到防止unsafe破坏的方法:
/**
* 单例模式
* 饿汉式
*/
public class Singleton1 implements Serializable {
// 构造函数
private Singleton1() {
// 防止反射创建对象破坏单例
if (instance != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("开始初始化实例======");
}
private final static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
// 一个其他方法用来测试执行该方法时会不会实例化
public static void otherMethod() {
System.out.println("执行其他方法======");
}
// 对于实现序列化的对象重写readResolve()方法,防止反序列化破幻单例
public Object readResolve() {
return instance;
}
}
执行结果如下: 执行反射创建时直接抛出异常,反序列化返回的实例也都是同一个实例
2.枚举饿汉式
/**
* 枚举饿汉式
*/
public enum Singleton2 {
INSTANCE;
private Singleton2() {
System.out.println("开始初始化实例======");
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public static Singleton2 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
执行结果如下: 枚举饿汉式能天然防止反射、反序列化破坏单例
3.双检锁懒汉式
/**
* 双检锁懒汉式
*/
public class Singleton3 implements Serializable {
private Singleton3() {
// 防止反射创建对象破坏单例
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("开始初始化实例======");
}
private static volatile Singleton3 INSTANCE = null; // volatile 保证不同线程内存可见性,执行有序性
public static Singleton3 getInstance() {
if (INSTANCE == null) {
// 加锁保证多线程并发执行
synchronized (Singleton3.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
// 对于实现序列化的对象重写readResolve()方法,防止反序列化破幻单例
public Object readResolve() {
return INSTANCE;
}
}
为何必须加 volatile:
INSTANCE = new Singleton3()不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造- 如果线程1 先执行了赋值,线程2 执行到第一个
INSTANCE == null时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象
执行结果如下:
可以看到在执行其他方法是并没有进行初始化实例,在调用 getInstance() 时才会进行初始化.
4.内部类懒汉式
/***
* 内部类懒汉式
*/
public class Singleton4 implements Serializable {
private Singleton4() {
// 防止反射创建对象破坏单例
if (Holder.INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("开始初始化实例======");
}
private static class Holder {
static Singleton4 INSTANCE = new Singleton4();
}
public static Singleton4 getInstance() {
return Holder.INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
// 对于实现序列化的对象重写readResolve()方法,防止反序列化破幻单例
public Object readResolve() {
return Holder.INSTANCE;
}
}
当外部类被访问时,并不会加载内部类,所以只要不访问Holder这个内部类,static Singleton4 INSTANCE = new Singleton4(); 不会实例化,这就相当于实现懒加载的效果,只有当Singleton4.getInstance() 被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了饿汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。
执行结果如下:
JDK 中单例的体现
- Runtime 体现了饿汉式单例
- Console 体现了双检锁懒汉式单例
- Collections 中的 EmptyNavigableSet 内部类懒汉式单例
- ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
- Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例