单例模式

76 阅读13分钟

模式介绍

所谓类的单例模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

八种实现方式

饿汉式(静态变量)

示例代码

package model;

/**
 * 饿汉式(静态变量)
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2); // 结果为true
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    // 2、本类内部创建对象实例
    private static final Singleton instance = new Singleton();

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

优缺点说明

1、优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题;

2、缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果一直都没使用过这个实例,则会造成内存的浪费;这种方式基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其它的方式(或者其它的静态方法)导致类装载,这时候初始化 instance 就没有达到 Lazy Loading 的效果;

3、结论:这种单例模式可用,可能会造成内存浪费。

饿汉式(静态代码块)

示例代码

package model;

/**
 * 饿汉式(静态代码块)
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static final Singleton instance;

    // 2、本类内部静态代码块中创建对象实例
    static {
        instance = new Singleton();
    }

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

优缺点说明

1、这种方式的优缺点跟饿汉式(静态变量)类似,只是将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。

2、结论:这种单例模式可用,可能会造成内存浪费。

懒汉式(线程不安全)

示例代码

package model;

/**
 * 懒汉式(线程不安全)
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static Singleton instance;

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点说明

1、起到了 Lazy Loading 的效果,但是只能在单线程下使用;

2、如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式;

3、结论:在实际开发中,不能使用这种方式。

懒汉式(线程安全,同步方法)

示例代码

package model;

/**
 * 懒汉式(线程安全,同步方法)
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static Singleton instance;

    // 提供一个公有的静态方法,返回实例对象
    public static synchronized Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点说明

1、解决了线程不安全问题;

2、效率太低了,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化就够了,后面的想获得该类实例,直接 return 就可以了。方法进行同步效率太低;

3、结论:在实际开发中,不推荐使用这种方式

懒汉式(线程安全,同步代码块)

示例代码

package model;

/**
 * 懒汉式(线程安全,同步代码块)
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static Singleton instance;

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

优缺点说明

1、这种同步不能起到线程安全的作用,在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例;

2、结论:在实际开发中,不能使用这种方式。

双重检查

示例代码

package model;

/**
 * 双重检查
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static volatile Singleton instance;

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优缺点说明

1、双重检查是进行了两次 if (singleton == null) 检查,这样就可以保证线程安全了;

2、实例化代码只需执行一次,往后再次访问的话,判断 if(singleton == null) ,直接 return 实例化对象,也避免了反复执行方法同步;

3、线程安全、延迟加载、效率较高;

4、结论:在实际开发中,推荐使用这种单例设计模式。

静态内部类

示例代码

package model;

/**
 * 静态内部类
 *
 * @author asyy
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

class Singleton {
    // 1、构造器私有化,外部不能new实例
    private Singleton() {

    }

    private static class SingletonInstance {
        private static Singleton instance = new Singleton();
    }

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
}

优缺点说明

1、采用了类装载的机制来保证初始化实例时只有一个线程;

2、静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化;

3、类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;

4、优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高;

5、结论:实际工作开发中,推荐使用。

枚举

示例代码

package model;

/**
 * 枚举
 *
 * @author asyyr
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.INSTANCE;
        Singleton singleton2 = Singleton.INSTANCE;
        System.out.println(singleton1 == singleton2); // true
        System.out.println(singleton1.hashCode() == singleton2.hashCode()); // true
        singleton1.getInstance();
    }
}

enum Singleton {
    INSTANCE;

    public void getInstance() {
        System.out.println("获取实例");
    }
}

优缺点说明

1、借助 JDK1.5 中添加的美剧来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象;

2、这种方式是 Effective Java 作用 Josh Bloch 提倡的方式;

3、结论:实际工作中推荐使用。

两种破坏方式

序列化破坏单例模式

package model;

import java.io.*;

/**
 * 使用序列化反序列化方式破坏单例
 *
 * @author asyyr
 */
public class SerialSingle {
    public static void main(String[] args) throws Exception {
        String filePath = "E:\selfCode\design-pattern\staticClass.txt";
        delFile(filePath);
        // 静态内部类 单例模式
        StaticClassDemo clazz = StaticClassDemo.getInstance();
        System.out.println("序列化前对象的地址:" + clazz);
        writeObjectToFile(clazz, filePath);
        Object object = readObjectFromFile(filePath);
        System.out.println("反序列化对象后的地址:" + object);
        // 判断两个反序列化后的对象是否是同一个对象
        System.out.println(clazz == object);
    }

    public static Object readObjectFromFile(String filePath) throws Exception {
        // 创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        Object object = ois.readObject();
        ois.close();
        return object;
    }

    public static void writeObjectToFile(Object object, String filePath) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        oos.writeObject(object);
        oos.close();
    }

    public static void delFile(String filePath) {
        File file = new File(filePath);
        file.deleteOnExit();
    }
}

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

注意:枚举方式不会出现这个问题。

解决方式

Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建出来的对象。

package model;

import java.io.Serializable;

/**
 * desc:静态内部类 单例模式,线程安全
 * @author asyyr
 */
public class StaticClassDemo implements Serializable {

    private static final long serialVersionUID = 888L;

    private StaticClassDemo() {
        System.out.println("实例化对象");
    }

    private static class InnerClassDemo {
        private static final StaticClassDemo INSTANCE = new StaticClassDemo();
    }

    public static StaticClassDemo getInstance() {
        return InnerClassDemo.INSTANCE;
    }

    /**
     * 当反序列化对象时,JVM会由readResolve返回指定对象,也就保证了单例;
     * 假如缺乏readResolve方法,反序列化获取的对象跟序列化时的对象不一致;
     *
     * @return
     * @throws Exception
     */
    protected Object readResolve() throws Exception {
        return StaticClassDemo.getInstance();
    }
}

源码分析

核心类:ObjectInputStream 类

public final Object readObject() throws IOException, ClassNotFoundException {
    ... 忽略其次代码
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
        Object obj = readObject0(false);
    ... 忽略其次代码
}
private Object readObject0(boolean unshared) throws IOException {
    ... 忽略其次代码
    case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));
    ... 忽略其次代码   
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
    ... 忽略其次代码 
    // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
        ... 忽略其次代码 
        // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod()方法执行结果为true
        if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
            // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
            // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
            Object rep = desc.invokeReadResolve(obj);
        }
    ... 忽略其次代码 
    return obj;
}

反射破坏单例模式

package model;

import java.lang.reflect.Constructor;

/**
 * 使用反射方式破坏单例
 *
 * @author asyyr
 */
public class ReflectSingle {
    public static void main(String[] args) throws Exception {
        // 获取Singleton类的字节码对象
        Class<StaticClassDemo> clazz = StaticClassDemo.class;
        // 获取Singleton类的私有无参构造方法对象
        Constructor<StaticClassDemo> constructor = clazz.getDeclaredConstructor();
        // 取消访问检查
        constructor.setAccessible(true);
        // 创建Singleton类的对象s1
        StaticClassDemo staticClassDemo1 = constructor.newInstance();
        System.out.println(staticClassDemo1);
        // 创建Singleton类的对象s2
        StaticClassDemo staticClassDemo2 = constructor.newInstance();
        System.out.println(staticClassDemo2);
        // 判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(staticClassDemo1 == staticClassDemo2);
    }
}

上面代码运行结果是false,表明可以通过反射破坏单例设计模式。

注意:枚举方式不会出现这个问题。

解决方式

package model;

import java.io.Serializable;

/**
 * desc:静态内部类 单例模式,线程安全
 * @author asyyr
 */
public class StaticClassDemo {

    private static final long serialVersionUID = 888L;

    private StaticClassDemo() {
        // 反射破解单例模式需要添加的代码
        if (InnerClassDemo.INSTANCE != null) {
            throw new RuntimeException("单例模式禁止反射创建实例");
        }
        System.out.println("实例化对象");
    }

    private static class InnerClassDemo {
        private static final StaticClassDemo INSTANCE = new StaticClassDemo();
    }

    public static StaticClassDemo getInstance() {
        return InnerClassDemo.INSTANCE;
    }
}

单例类的状态

有状态的单例类

一个单例类可以是有状态的,一个有状态的单例对象一般也是可变(mutable)单例对象。

有状态的可变的单例对象常常当做状态库(repositary)使用。比如一个单例对象可以持有一个 int 类型的属性,用来给一个系统提供一个数值唯一的序列号码,作为某个贩卖系统的账单号码。

当然,一个单例类可以持有一个聚集,从而允许存储多个状态。

无状态的单例类

单例类可以是没有状态的,仅用作提供工具性函数的对象。既然是为了提供工具性函数,也就没有必要创建多个实例,因此使用单例模式很合适。一个没有状态的单例类也就是不变(Immutable)单例类。

多个 JVM 系统的分散式系统

EJB 容器有能力将一个 EJB 的实例跨过几个 JVM 调用。由于单例对象不是 EJB,因此,单例类局限于某一个 JVM 中,换言之,如果 EJB 在跨过 JVM 后仍然需要引用同一个单例类的话,这个单例类就会在数个 JVM 中被实例化,造成多个单例对象的实例出现。一个 J2EE 应用系统可能分布在数个 JVM 中,这时候不一定需要 EJB 就能造成多个单例类的实例出现在不同 JVM 中的情况。

如果这个单例类是没有状态的,那么就没有问题。因为没有状态的对象是没有区别的。但是如果这个单例类是有状态的,那么问题就来了,举例来说,如果一个单例对象可以持有一个 int 类型的属性,用来给一个系统提供一个数值唯一的序列号吗,作为某个贩卖系统的账单号码的话,用户会看到同一个号码出现好几次。

多个类加载器

同一个 JVM 中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例。在很多 J2EE 服务器允许同一个服务器内有几个 Servlet 引擎时,每一个引擎都有独立的类加载器,经由不同的类加载器加载的对象之间是绝缘的。

除非系统有协调机制,不然应当尽量避免使用有状态的单例类。

注意事项

1、单例模式保证了系统内存中只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;

2、当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new

3、单例模式使用的场景:需要频繁的进行创建和销毁的对象,创建对象时耗时过多或者耗费资源过多(即重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或者文件的对象(比如数据源、session 工厂等)。

4、一般而言,双重检查对 Java 编译器不成立。由于类的初始化与变量赋值的顺序不可预料,如果一个线程在没有同步化的条件下读取变量引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。

应用场景

JDK Runtime 类

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

从上面源代码中可以看出Runtime类使用的是饿汉式(静态变量)方式来实现单例模式的。

package model;

import java.lang.reflect.Constructor;

/**
 * 使用反射方式破坏单例
 *
 * @author asyyr
 */
public class ReflectSingle {
    public static void main(String[] args) throws Exception {
        // 获取Singleton类的字节码对象
        Class<Runtime> clazz = Runtime.class;
        // 获取Singleton类的私有无参构造方法对象
        Constructor<Runtime> constructor = clazz.getDeclaredConstructor();
        // 取消访问检查
        constructor.setAccessible(true);
        // 创建Singleton类的对象s1
        Runtime staticClassDemo1 = constructor.newInstance();
        System.out.println(staticClassDemo1);
        // 创建Singleton类的对象s2
        Runtime staticClassDemo2 = constructor.newInstance();
        System.out.println(staticClassDemo2);
        // 判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(staticClassDemo1 == staticClassDemo2);
    }
}

上面代码运行结果是false,表明可以通过反射破坏Runtime类实现的单例模式。但是由于Runtime没有实现Serializable,没法通过序列化方式破坏单例模式。