为什么需要单例模式?
有句话很好的回答了这个问题
有一个就不错了,多了负担不起,也容易乱。
以前有个动画片叫《天书奇谈》
主角有个聚宝盆,能把任何进聚宝盆里的东西复制n份,有一集是县太爷的老父亲掉到聚宝盆里了,然后。。县太爷多了好多个爸爸。县太爷一叫爸爸,十几个答应的。
爸爸只能有一个,也只能是那一个,这就是单例,叫一声爸爸十几个答应的实在受不了。
程序中的单例
单例就是只有一个实例的意思
写一个单例的套路
基本要求
-
提供私有化构造方法
-
提供getInstance方法,又叫全局访问点,其实就是暴露一个得到实例的窗口
需要解决的问题
- 解决资源消耗问题
- 解决多线程破坏单例问题
- 解决反射破坏单例问题
- 解决序列化破坏单例问题
详细说说
最简单的单例写法就是私有化一个构造器,再利用java类初始化顺序中静态成员先执行的语法特点,在类初始化时就创建实例。这种方法叫饿汉式,意思是说不管用不用先怼上再说。
public class HungrySingleton {
//类初始化静态成员先执行,调用私有构造方法
private static final HungrySingleton hungrySingleton = new HungrySingleton();
//私有化构造方法
private HungrySingleton(){}
//提供一个全局访问点
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
这种方式好处明显,就是简单,执行效率高没啥乱七八糟的东西。缺点也很明显,不管需不需要,初始化的时候上来就怼上个实例,这要是系统中很多实例都要求单例,用饿汉式的话系统资源消耗是巨大的。
更合理的来说,应该是有需要的时候再进行类初始化,这样能防止资源的消耗。延迟加载的单例方式叫是懒汉式单例。
public class LazySingleton {
//volatile关键字禁止指令重排序
//与饿汉式区别:初始化时没调用私有化构造方法,在getInstance时才调用
private volatile static LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
//解决线程安全问题
//instance为空再阻塞,因为此时要获得类实例
if (instance == null) {
synchronized (LazySingleton.class) { //如果两个线程走到此处,只能有一个线程执行下
//面代码,另一个会被阻塞
//判断是否要重新创建实例,这是因为如果第一个进来的线程创建了对象,
//实际上另一个被阻塞的线程在前一个线程释放锁之后还是会进来执行下面的代码的,
//所以必须再判断一次null,防止实例被重复创建。
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
懒汉式单例首先面临的是多线程的问题,要解决这个问题就要使用synchronized关键字,给类添加必要的锁,以及必要的判断条件,同时防止指令重排序导致的问题,需要使用volatile关键字,相关说明已写在代码中。
另一种延迟加载的方法可以使用静态内部类延迟加载的语法特点,降低实现的复杂程度。
public class LazyStaticInnerClassSingleton {
//注意这里!!!
private LazyStaticInnerClassSingleton() {
if(LazyHolder.INSTANCE != null) {
throw new RuntimeException("不允许反射破坏单例!");
}
}
private static LazyStaticInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
调用getInstance()方法时,才才会加载LazyHolder静态内部类,实现懒加载。这样实现比上面的双重检查的方式代码结构清晰了很多。但是也暴露了以上方法中一个潜在的问题,就是反射会破坏单例。
要说这个反射机制在java里可是非常牛ABCD的,几乎是想干啥就能干啥,可以说是代码中的root权限了。
虽然我在类中定义了私有的构造函数,但是通过反射完全可以轻松绕过。
//获得参构造函数
Constructor<?> c = clazz.getDeclaredConstructor();
//设置私有成员可访问,通过反射即使是private也没法保护成员了
c.setAccessible(true);
//想创建多少个实例都可以
Object instance1 = c.newInstance();
//再创建一个
Object instance2 = c.newInstance();
这样单例实际上就不是单例了。所以LazyStaticInnerClassSingleton类中在私有构造方法里要判断不能重复创建来保证单例。
另外还有一种更巧妙的单例写法,是下面这本书推荐的。
使用枚举的特性实现不被反射破坏的单例。
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){return INSTANCE;}
}
这样的单例实现方式,在我们用反射调用的使用是会报错的。
提示我们不能通过反射方式创建枚举对象,这是java语言层约定的规则,从根本上保证了反射破坏单例的情况。
不过这种单例本质上还是饿汉式单例,仍然会造成内存浪费。
在spring中,单例实现是基于容器也就是ConcurrentHashMap的,像下面这样:
public class ContainerSingleton {
private static final Map<String, Object> container = new ConcurrentHashMap<String, Object>();
private ContainerSingleton(){}
public static void putInstance(String key, Object instance) {
if (key.length()>0 && instance != null) {
synchronized (container){
if (!container.containsKey(key)) {
container.put(key, instance);
}
}
}
}
public static Object getInstance(String key) {
return container.get(key);
}
}
这样能节省资源,同时注意ConcurrentHashMap线程安全问题。
单例还面临一个问题就是序列化破坏单例的情况,这主要是因为序列化过程中使用ObjectInputStream读取磁盘中保存的序列化结果,而这个类会有一个判断,那就是判断对象是否有readResolve()方法,在没有的情况下会新建对象,就破坏了单例。
我们要解决序列化破坏单例的问题也很简单,只需要在类中创建readResolve()方法即可。
private Object readResolve(){ return INSTANCE;}