单例模式是我们日常工作中接触最多,也可以说是最简单的一个设计模式。那么什么是单例呢?为什么要用这种模式?
单例简单来说就是只进行一次初始化,以Imageloader为解释为什么要用这种模式,因为ImageLoader会涉及到大量的io和网络请求,消耗大量的资源,为了减少资源的消耗和浪费,便出现了单例模式。
我们常见的单例模式有很多种实现方式,比如饿汉模式、懒汉模式、双加锁模式等等,下面以简单的例子来一一记录这些单例模式的实现:
1、饿汉模式
为何称之为“饿汉模式”呢?因为饿汉经常会突然的饿意袭来,为了在饿意到来的时候,有足够的食物,饿汉需要提前将食物准备好。我们的“饿汉模式”也是这样,在使用之前便为我们初始化了单例对象,需要注意的是初始化的时候,还没有产生子线程,因此不会存在线程安全的问题。
public class Hungry {
private static Hungry INSTANCE = new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return INSTANCE;
}
}
2、懒汉模式
该模式是经常与饿汉一起提起的,为什么叫懒汉呢?是因为该模式下不会提前进行初始化,而是在需要的时候才会进行初始化。在这种模式下,需要注意的是高并发情况下,会出现线程安全问题,因此需要引入同步锁!
public class Lazy {
private static Lazy INSTANCE = null;
private Lazy(){}
public static synchronized Lazy getInstance(){
if(INSTANCE == null){
INSTANCE = new Lazy();
}
return INSTANCE;
}
}
“饿汉模式”与“懒汉模式”的区别在于初始化的时间不同,“饿汉模式”下无论是否会调用,都会进行初始化,造成资源的浪费,而“懒汉模式”只会在需要的时候初始化,不会造成资源浪费。
3、双加锁(DCL)模式
在“懒汉模式”,虽然不会每次调用getInstance都会进行初始化,但是每次调用都会使用同步锁,也会造成不必要的浪费,因此出现了“双加锁模式”,代码如下:
public class DoubleCheckLocked {
private static DoubleCheckLocked INSTANCE;
private DoubleCheckLocked(){}
public static DoubleCheckLocked getInstance(){
if(INSTANCE == null){
synchronized (DoubleCheckLocked.class){
if(INSTANCE == null){
INSTANCE = new DoubleCheckLocked();
}
}
}
return INSTANCE;
}
}
从上面的代码,我们可以看到两次判空操作,第一次判空是为了判断是否需要加锁,而第二次判断则是为了保证只会初始化一次。这么看来,这种模式是不是一种最优的方案呢?实际上是不支持这种写法的,因为会有一个叫做“重排序”的问题,重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
正常情况下,INSTANCE = new DoubleCheckLocked();这句代码可以分为三个步骤:
a.分配对象的内存空间
b.初始化对象
c.设置sInstance指向刚分配的内存地址
但是由于重排序的存在,在高并发的环境下,为了提高运行速率,可能会让bc的步骤发生变化,这样就有可能导致某个线程调用获取到了一个还未初始化的对象。
JDK1.5之后针对上面的问题,官方对volatile进行了优化,因此在1.6之后也可以通过volatile实现。
4、静态内部类实现单例
public class StaticInnerClass {
private StaticInnerClass(){}
public static StaticInnerClass getInstance(){
return SingleClass.INSTANCE;
}
private static class SingleClass{
private static final StaticInnerClass INSTANCE = new StaticInnerClass();
}
}
因为INSTANCE 的定义是private static final,因此只会在第一次调用的时候才会初始化,因此不会涉及线程安全问题。另外外部类加载的时候并不会进行初始化,因此不会占用内存。
当然除了上面四种方式,我们还可以通过枚举或者一些容器,如HashMap等实现单例,枚举无需解释,本身枚举就是为单例而生,而使用容器等可能会涉及到线程安全问题,在某些场景下使用需要思考清楚。
5、枚举实现单例
public class Enum {
public enum EnumEnum{
ENUM;
private EnumEnum(){
INSTANCE = new Enum();
}
private Enum INSTANCE;
public Enum getINSTANCE(){
return INSTANCE;
}
}
private Enum(){}
public static Enum getInstance(){
return EnumEnum.ENUM.getINSTANCE();
}
}
6、容器实现单例
public class Container {
private static Map<String,Container> SINGLEMAP = new HashMap<>();
public static void putSingle(String key,Container value){
if(!SINGLEMAP.containsKey(key)){
SINGLEMAP.put(key,value);
}
}
public static Container getInstance(String key){
return SINGLEMAP.get(key);
}
}
综上所述,实现单例的方法有很多种,其核心就是为了减少初始化的次数,从而减少资源的浪费,多用在涉及到IO、网络请求等场景。道路千万条,针对不同的场景,选择最合适的方案!