单例模式
单例模式一定安全吗?哪些方式可以破坏单例模式?
单例模式不一定安全,以下几种方式可以破坏单例模式:
- 多线程环境下的线程安全性:
- 如果多个线程同时访问单例对象的获取方法,可能会导致多次创建实例或出现竞态条件,从而破坏单例模式。
- 解决方法:可以通过加锁(如使用synchronized关键字或ReentrantLock)来确保在多线程环境下只有一个线程可以创建实例,来保证单例。
- 序列化和反序列化:
- 当单例类实现了Serializable接口时,单例对象可以被序列化和反序列化。在反序列化时,会创建一个新的对象,从而破坏了单例模式。
- 解决方法:可以通过重写readResolve()方法,在反序列化时返回单例对象,确保反序列化后仍然是同一个单例对象。
- 反射:
- 通过反射机制可以调用类的私有构造方法来创建对象,从文破坏单例模式。
- 解决方法:可以在构造方法中添加逻辑,防止多次实例化;或这在创建第二个实例时抛出异常。
- 类加载器:
- 在一些特殊情况下,可能会存在多个类加载器加载同一个类,导致创建多个单例对象的情况。
- 解决方法:可以通过控制类加载器的方式来确保只有一个类加载器加载单例类。
- 多个实例:
- 单例模式的实现可能并不完全符合单例模式的定义,例如通过静态变量或静态方法来实现单例,但在多个类加载器的情况下可能会创建多个单例对象。
- 解决方法:确保单例模式的实现符合单例模式的定义,同时考虑类加载器的影响。
- 拷贝可以破坏单例
定义
确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
饿汉式
饿汉式单例模式就是在类加载的时候就立即初始化,并且创建单例对象。
不管你有没有用到,都先建好了再说。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
优点
线程安全,没有加任何锁、执行效率比较高。
缺点
类加载的时候就初始化,不管后期用不用都占着空间,浪费内存,不推荐这样使用单例。
代码示例
/**
* 单例模式
* 饿汉式:类加载时就立即初始化,并且创建单例对象。
* 线程安全,占用资源
*/
public class HungrySingleton {
//先静态,后动态;先属性,后方法;先上后下;
private static final HungrySingleton hungrySingleton = new HungrySingleton();
//构造函数私有
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
/**
* 单例模式
* 饿汉式(静态代码块实现):类加载时就立即初始化,并且创建单例对象。
* 线程安全,占用资源
*/
public class HungryStaticSingleton {
//先静态,后动态;先属性,后方法;先上后下;
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
//构造函数私有
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
懒汉式
实例在用到的时候才去检查有没有实例,如果有则直接返回,没有则新建
/**
* 单例模式
* 懒汉式:实例在用到时才去检查有没有实例,没有则创建,有则返回。
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
//构造函数私有
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
上面这种写法不是线程安全的:
在多线程情况下,如果两个线程同时走到if (lazySimpleSingleton == null)这个判断,因为lazy还没有实例化过,所以两个线程判断的结果都是true,然后同时进入代码块创建了多个实例,返回不是同一个对象,线程不安全。
/**
* 单例模式
* 懒汉式:实例在用到时才去检查有没有实例,没有则创建,有则返回。
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
//构造函数私有
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
可以加锁synchronized修饰解决线程安全问题,但是在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。所以为了兼顾性能和线程安全问题,可以使用双重检查锁方式来创建懒汉式单例:
/**
* 单例模式
* 懒汉式:实例在用到时才去检查有没有实例,没有则创建,有则返回。
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
//构造函数私有
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
synchronized (LazySimpleSingleton.class) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
return lazySimpleSingleton;
}
}
使用synchronized关键字总归要上锁,对程序性能还是存在一定影响。
/**
* 静态内部类方式
* 兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
*/
public class LazyInnerClassSingleton {
//使用LazyInnerClassSingleton的时候,默认会先初始化内部类
//如果没有使用,则内部类是不加载的。
private LazyInnerClassSingleton() {
}
//static是为了使单例的空间共享,保证这个方法不会被重写、重载
public static final LazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类。
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例的内存浪费问题和synchronized的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。
反射破坏单例
饿汉式和懒汉式单例都是将构造函数私有,用来防止通过new来创建对象实例。
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws Exception {
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
//通过反射获取私有的构造函数
Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
//暴力初始化
LazyInnerClassSingleton singleton = c.newInstance();
//调用两次构造方法,相当于new了两次,违反单例原则。
LazyInnerClassSingleton instance = singleton.getInstance();
System.out.println(singleton == instance);//false
}
}
/**
* 静态内部类方式
* 兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
*/
public class LazyInnerClassSingleton {
//使用LazyInnerClassSingleton的时候,默认会先初始化内部类
//如果没有使用,则内部类是不加载的。
private LazyInnerClassSingleton() {
//防止反射破坏单例
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
//static是为了使单例的空间共享,保证这个方法不会被重写、重载
public static final LazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类。
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,会重新分配内存,也就是重新创建对象。就会破坏单例。
/**
* 反序列化破坏单例
*/
public class SeriableSingleton implements Serializable {
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换一个I/O流,写入磁盘或网络I/O
//内存中的状态会永久保存下来
//反序列化就是将已经持久化的字节码内容转化为I/O流
//通过I/O流的读取,进而将读物的内容转换为Java对象
//在转换过程中会重新创建对象new
private static final SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
}
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton instance = SeriableSingleton.getInstance();
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
//序列化
FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
//反序列化
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ois = new ObjectInputStream(fis);
SeriableSingleton instance2 = (SeriableSingleton) ois.readObject();
System.out.println(instance2 == instance);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
if (oos != null) {
oos.close();
}
} catch (Exception e) {
}
}
}
}
结果为false,可知反序列化的对象和原始对象不是同一个,打破了单例。
解决方案:只需增加readResolve()方法即可。
public class SeriableSingleton implements Serializable {
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换一个I/O流,写入磁盘或网络I/O
//内存中的状态会永久保存下来
//发序列化就是将已经持久化的字节码内容转化为I/O流
//通过I/O流的读取,进而将读物的内容转换为Java对象
//在转换过程中会重新创建对象new
private static final SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
//解决反射导致破坏单例的问题
private Object readResolve(){
return INSTANCE;
}
}
通过源码可知底层是通过反射来找到一个无参的readResolve()方法,并返回该方法返回值。
虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。
注册式单例模式
注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。
注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
序列化和反序列化不会破坏枚举式单例。
通过反射也不能破坏枚举式单例,会报错找不到无参构造器,无法使用反射创建枚举。
容器式单例
/**
* 容器式单例
*/
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getBean(String className) {
synchronized (ioc) {
if (ioc.containsKey(className)) {
return ioc.get(className);
} else {
Object obj = null;
try {
//通过类加载器来创建对象
obj = Class.forName(className);
ioc.put(className, obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return obj;
}
}
}
}
容器式单例模式适用于实例非常多的情况,便于管理。但是它是非线程安全的。