对于Java来说单例模式可以有以下几种方式:
- 饿汉方式
- 懒汉方式
- 双重检查加锁懒汉方式
- 内部类方式
- 枚举方式
破解单例模式有两种方式:通过反射的方式和通过序列化的方式。下面将一一对此进行分析。
饿汉方式非常简单,即使用一个初始化的静态变量,代码如下:
1 |
public class EagerSingleton { |
2 |
private static final EagerSingleton instance = new EagerSingleton(); |
4 |
private EagerSingleton(){} |
6 |
public static EagerSingleton getInstance() { |
懒汉模式和饿汉模式类似,只是静态变量定义时不进行初始化,调用getInstance()时才进行初始化,这就需要考虑多线程时问题,使用synchronized关键字修饰方法即可:
02 |
private static LazySingleton instance = null ; |
03 |
private LazySingleton() {} |
04 |
public static synchronized LazySingleton getInstance() { |
05 |
if (instance == null ) { |
06 |
instance = new LazySingleton(); |
懒汉模式实际是一种懒加载,但是为了避免多线程时单例失效,必须对getInstance()方法进行同步。可以使用双重检查方式来避免对方法全部进行加锁:
01 |
class DoubleCheckSingleton { |
02 |
private static volatile DoubleCheckSingleton instance = null ; |
03 |
private DoubleCheckSingleton() {} |
04 |
public static DoubleCheckSingleton getInstance() { |
05 |
if (instance == null ) { |
06 |
synchronized (DoubleCheckSingleton. class ) { |
07 |
if (instance == null ) { |
08 |
instance = new DoubleCheckSingleton(); |
这个需要几个注意点:静态类变量必须声明为private static volatile,这样可以 写入操作 happens-before 于每一个后续的同一个字段的读操作。在getInstance()方法中,第一次判断null后,使用同步防止多线程时破坏单例。
第四种是使用内部类的方式,也是一种懒加载的方式实现:
1 |
class InnerClassLazySingleton { |
2 |
private static class SingletonCreator { |
3 |
private static final InnerClassLazySingleton instance = new InnerClassLazySingleton(); |
5 |
public static InnerClassLazySingleton getInstance() { |
6 |
return SingletonCreator.instance; |
内部类只在第一次调用的时候才会被类加载器加载,实现了懒加载
同时由于instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性
但是上面四种方式都可以通过反射的方式来破坏单例:
1 |
InnerClassLazySingleton eagerSingleton = InnerClassLazySingleton.getInstance(); |
2 |
Constructor<InnerClassLazySingleton> constructor = InnerClassLazySingleton. class .getDeclaredConstructor(); |
3 |
constructor.setAccessible( true ); |
5 |
InnerClassLazySingleton eagerSingletonFromRef = constructor.newInstance(); |
6 |
System.out.println( "使用反射破解饥渴单例模式:" + (eagerSingleton == eagerSingletonFromRef ? "否" : "是" )); |
并且如果实现了Serializable接口的话,通过序列化的方式也可以破坏单例:
1 |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
2 |
ObjectOutputStream oos = new ObjectOutputStream(baos); |
3 |
oos.writeObject(eagerSingleton); |
5 |
ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(baos.toByteArray())); |
6 |
InnerClassLazySingleton eagerSingletonFromSerial = (InnerClassLazySingleton) ois.readObject(); |
7 |
System.out.println( "使用序列化破解饥渴单例模式:" + (eagerSingleton == eagerSingletonFromSerial ? "否" : "是" )); |
可以通过在单例类中添加readResolve()方法的方式来解决:
01 |
class EagerSingleton implements Serializable { |
02 |
private static final EagerSingleton instance = new EagerSingleton(); |
03 |
private EagerSingleton() {} |
04 |
public static EagerSingleton getInstance() { |
08 |
private Object readResolve() { |
09 |
System.out.println( "print from readResolve() method" ); |
13 |
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
14 |
System.out.println( "print from readObject() method" ); |
15 |
ois.defaultReadObject(); |
readResolve()方法是用来替换从流中读取的对象的,它在readObject(ObjectInputStream)方法之后被调用,readObject()方法即从流中读取对象的方法。这样就可以避免使用序列化方式破坏单例。
但是上面的四种方式还是无法避免反射的方式来破坏单例的情况,可以使用枚举的方式实现单例:
4 |
public static void method() { |
通过反射方式创建实例时会抛出 Exception in thread "main" java.lang.NoSuchMethodException: net.local.singleton.EnumSingleton.<init>() 异常。并且使用枚举类Enum实现了序列化接口,并且也实现了readResolve()方法,因为使用序列化方式也没法破坏,是最理想的单例模式实现。