【设计模式】- 单例模式

138 阅读5分钟

单例模式

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

为什么要用单例模式

在系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

单例模式带来的好处

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

为什么不使用全局变量确保一个类只有一个实例

静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。

单例模式写法

饿汉(可用)

public class Singleton {
    private static Singleton instance=new Singleton();
    private Singleton(){

    }
    public static Singleton getInstance(){
        return instance;
    }
}

优缺点

优:在类加载的时候就完成了实例化,避免了多线程的同步问题,另外在每次调用时,无须对对象进行null判断

缺:因为类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,便浪费了内存空间。

懒汉

懒汉第一版(不可用)

public class Singleton {
    private Singleton() {}                     //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

问题:

会出现多线程安全问题

懒汉第二版(不可用)

public class Singleton {
    private Singleton() {}                     //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {                //双重检测机制
            synchronized(Singleton.class){     //同步锁
                if(null == instance){          //双重检测机制
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

说明

  • 1.为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。
  • 2.进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

问题

会因重排序导致多线程问题,不是绝对的线程安全

在java中,对于instance = new Singleton,会被编译器编译成如下jvm指令

memory =allocate();     //1:分配对象的内存空间
ctorInstance(memory);   //2:初始化对象
instance =memory;       //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate();     //1:分配对象的内存空间
instance =memory;       //3:设置instance指向刚分配的内存地址
ctorInstance(memory);   //2:初始化对象

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。

懒汉第三版/双重检查DCL(可用)

public class Singleton {
    private Singleton() {}                     //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {                //双重检测机制
            synchronized(Singleton.class){     //同步锁
                if(null == instance){          //双重检测机制
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

说明:

1.添加volatitle,用于禁止指定重排序

静态内部类

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

说明:

1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。

2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

枚举

enum SingletonDemo{
    INSTANCE;
    public void otherMethods(){
        System.out.println("Something");
    }
}

使用

public class Hello {
    public static void main(String[] args){
        SingletonDemo.INSTANCE.otherMethods();
    }
}

说明

《Effective Java 》以及《Java与模式》的作者推荐的方式。

优缺点:

优:可以阻止反射获取枚举类的私有构造方法,同时可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象

缺:A非懒加载模式,B当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

总结

上述方法实现存在的问题

除了枚举实现外,其它实现方法均存在以下问题

  • 无法防止利用反射来重复构建对建
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
  • 反序列化的问题

如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。

解决方式:添加readResolve()方法:

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     

   protected Singleton() {     

   }     
   private Object readResolve() {     
            return INSTANCE;     
    }    
}   

为什么反序列化会出现这样的问题:

www.hollischuang.com/archives/11…

对比(还没写)

方法
饿汉