单例模式由于只创建了唯一对象可以避免资源的多重占用,减少内存的开销,对于经常性使用对象的类来说,单例是一个不错的选择,使用场景,比如:文件操作、共享资源等等。
1、饿汉单例模式
这是最为简单的也是最基本的单例模式,相信大家都写过!先上代码:
public class Single{
private static final Single instance = new Single();
private Single(){}
public static Single getInstance(){
return instance;
}
}
由于把构造函数变为了private,所以要想获得该类的实例需要通过getInstance(),而不是手动去new一个,这仅仅是最为简单粗暴的单例模式。
2、懒汉单例模式
public class Single{
private static Single instance = null;
private Single(){}
public static synchronized Single getInstance(){
if (instance == null){
instance = new Single();
}
return instance;
}
}
懒汉单例模式看起来和饿汉单例模式差不多,就多了一个 synchronized 关键字,也就是说该模式是同步的方法单例。在 getInstanc()方法中,可以清楚知道不管该类对象是否已经实例了(实际上第一次调用的时候只new了一次),都会进行同步,这样确实每次都同步确实耗费了不必要的资源和内存,而且在加载的时候都会事先 synchronized,所以会比较耗时间,所以不太建议使用。
3、Double Check Lock(DCL)
public class Single{
private static Single instance = null;
private Single(){}
public static Single getInstance(){
if (instance == null){
synchronized (Single.class){
if (instance == null){
instance = new Single();
}
}
}
return instance;
}
}
在代码中可以看到,在调用 getInstance() 方法的时候会检查类的实例是否为空,true则直接返回该实例,false则会 synchronized (利用Single.class来对本类对象同步),如果为null则会new一个对象,否则直接返回。这意思很清楚,这里有两次判断是否为null的步骤,第一步判断是为了避免不必要的同步,而第二步则是同步检查。
我们知道在new,即instance = new Single()的时候,一般会有三个步骤:1、分配内存;2、调用构造函数并初始化成员字段;3、为对象指向分配好的空间。而Java是允许处理器乱序执行的,还有JDK版本原因,很多时候这三个步骤很可能不是按照顺序执行的,
所以在多线程的操作下,如果在A线程中执行某一步的时候,B线程也调用了该方法,而这时候A线程刚刚好分配内存成功(假设第一个调用)但未调用构造函数创建对象,所以这时候B在检查的时候对象已经非空了(因为已经指向了内存),所以B线程会直接使用对象instance,很显然,由于还没调用构造函数,所以B线程使用的时候会出问题。这叫DCL失效。虽然说这是很小的概率问题,但还是会长期隐藏着问题的。不过从JDK1.5之后,sun注意了这个问题,把这个bug修改了过来,只要 如此声明:private volatile static Single instance = null 就可以保证instance 是从主内存取出来的,虽然volatile 会影响性能,但为了DCL有效就值得。
总的来说DCL是饿汉、懒汉单例模式的结合,尽管存在bug,但还是sun已修改了,所以比较建议这种写法。
4 、内部静态类单例模式
利用静态内部类的特性来返回对象(static关键字不用多讲了吧)
public class Single {
private Single (){}
public static Single getInstance(){
return SingleHolder.instance;
}
private static class SingleHolder{
private static final Single instance = new Single();
}
}
这不仅仅首次调用才初始化,而且线程安全,也能保证对象的唯一性,所以这也是推荐的写法。
5、枚举单例模式
public enum SingleEnum {
INSTANCE;
}
在Java中,枚举类型是默认线程安全的,在任何情况下都能保证唯一性,而且写法最为简单。大家可以尝试一下的
6、容器单例模式
public class SingleCollect {
private static Map<String, Object> objectMap = new HashMap<>();
private SingleCollect(){}
/**
* 根据类名把实例通过Map保存起来
* @param key 类名
* @param instance 类的实例对象
*/
public static void registInatance(String key, Object instance){
if (!objectMap.containsKey(key)){
objectMap.put(key, instance);
}
}
/**
* 根据类名来寻找对应的对象实例
* @param key
* @return
*/
public static Object getInstance(String key){
return objectMap.get(key);
}
}
利用Map把类的对象实例保存起来,在需要的时候根据类名key获取即可。
总结
通过几种单例模式,核心思想无非是把构造函数私有化,然后利用 public static修饰符来获取对象实例,并且保证线程安全!!!值得注意的时候,我们还需要考虑这么一种情况:反序列化。我们知道可以通过序列化把对象实例写进磁盘,然后再读回来。而反序列化依然可以通过别的途径去重新创建一个新的对象实例,即便是私有的构造函数!上述的几种模式就只有枚举单例模式可以避免。不过,在实际开发中,我们需要结合项目需要,而不是一味地直接使用枚举单例模式,灵活使用单例模式才是正道。
上文如有不对或者不妥之处,大家记得留言指出哈。一起进步才比较爽啊!!!and then 后续我会继续写一系列关于设计模式的,请大家静候!