【java】单例模式

272 阅读4分钟

单例模式的定义:单例对象的类必须保证只有⼀个实例存在。

单例模式的优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。同时可避免对共享资源的多重占用。

缺点:不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 可读性差,想实例化一个单例类的时候,需要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

使用场合: 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 有状态的工具类对象。 频繁访问数据库或文件的对象。

单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。

饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用。

而懒汉式在调用取得实例方法的时候才会实例化对象。代码如下:

//单例-饿汉式
public class Singleton {

    private static final Singleton1 instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

}
//单例-懒汉式
public class Singleton {

    private Singleton() {}

    public static Singleton getInstance() {
        return Holder.SINGLETON;
    }

    private static class Holder {
        private static final Singleton SINGLETON = new Singleton();
    }
}

每个人的单例模式的写法可能会不太一样。以饿汉式为例,有的会写在静态常量中完成实例化,有的会在写在静态代码块中完成实例化。

但总的来说万变不离其宗,大家总结一下会发现单例模式包含这3个要素:

私有的构造方法
指向自己实例的私有静态引用
以自己实例为返回值的静态的公有的方法。
题外话(其实是为了预防有人忘了所以提一嘴):
静态变量和静态代码块是类加载的时候执行。
静态方法调用的时候才会初始化执行。

因此可以看到上面演示用的代码,饿汉式是在静态变量中完成实例化。
而懒汉式是在静态方法中完成实例化。

上文中的懒加载代码只是为了演示和饿汉式的区别,但只能在单线程下使用。如果在多线程下会产生多个实例。所以在多线程环境下不可使用这种方式。

一般会使用双重检查锁来完成懒汉式的实例化:

//单例-懒汉式
public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {    
        if (null == instance) {                  //1
            synchronized (Singleton.class) {     //2
                if (null == instance) {          //3
                    instance = new Singleton(); //4
                }
            }
        }
        return instance;                        //5
    }
  }  

Volatile在这里的作用是禁止指令重排。为什么要在这个例子里加这个关键字呢?

这是因为在满足特定条件下,处理器和编译器会对指令进行重排序,

a分配内存空间
b初始化对象
c将对象只想刚分配的内存空间

有些编译器为了性能的原因,可能会将第⼆步和第三步进⾏重排序,顺序就成了:

a分配内存空间
c将对象指向刚分配的内存空间
b初始化对象

在刚才的饿汉式单例例子中(我标注了5行),当线程A在执行第4行代码时,B线程进来执行到第1行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第5行并返回一个未初始化的对象。

因此为了避免这种情况会加volatile关键字来禁止重排。

单例模式是能够被反射和反序列化破坏所破坏的。我们一般会采取下列方案来解决:

对于反射,我们可以在单例的私有构造器中添加判断单例是否已经构造的代码,如果单例之前已经构造,则抛出异常。

对于反序列化,我们可以定义readResolve方法,直接返回方法指定的对象,而不会单独再创建对象。

代码如下(以懒汉式为例):

//单例-懒汉式
public class Singleton implements Serializable{
    private volatile static Singleton instance;

    //在构造器中做判断,如果单例被破坏则抛异常
    private Singleton() {
            if (null != instance) {
            throw new RuntimeException();
        }
    }

    public static Singleton getInstance() {    
        if (null == instance) {                  //1
            synchronized (Singleton.class) {     //2
                if (null == instance) {          //3
                    instance = new Singleton(); //4
                }
            }
        }
        return instance;                        //5
    }
    
    	//反序列化时,如果定义了readResolve方法,则直接返回此方法指定的对象,而不会再创建对象
	private Object readResolve() throws ObjectStreamException{
		return instance;
	}
    
  }