欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

浅析单例模式

程序员文章站 2022-07-14 09:36:50
...

单例是什么?

 单例是指某个类在整个系统中只有且仅有一个实例对象,即第一次创建后,后续创建的实例都是返回第一次创建时的实例。

单例有什么用?

 单例模式保证类在系统中只有一个实例,在某些应用中很实用,如定义一个全局的ID计数器,定义系统的运行环境等,都需要保证系统只存在一个实例对象。

单例模式的特点

  • 单例类必须只有一个实例
  • 单例类必须只有私有构造函数
  • 单例类必须对外提供获取唯一的实例的接口(或者说方法)

单例模式如何实现?

 单例模式的实现方式有很多种,常见的有懒汉式和饿汉式实现方式,除此之外还有利用枚举实现、利用静态内部类实现等,以下将一一介绍:

饿汉式单例模式

 这种实现方式是在类加载的时候就创建出唯一的实例对象,所以称之为“饿汉”,如下实现代码:

/**
 * Hungry Singleton
 * @author Horizon
 *
 * @Date 2019年6月4日下午8:13:21
 */
public class Singleton {
    
    // 静态变量,因需提供静态工厂方法给外部访问,直接new出来,在类加载时就创建实例
    private static Singleton instance = new Singleton();
    
    // 私有构造函数,禁止外部直接构造
    private Singleton() {}
    
    // 静态工厂方法,为外部提供唯一的实例
    public static Singleton getInstance() {
        return instance;
    }
    
}

 这种方式的好处就是简单方便,而且能够在多线程并发获取实例时,保证实例对象的唯一性,即不存在线程安全问题。但缺点就是如果该类未被使用到时,实例对象也会存在系统中,占用系统的资源。

懒汉式单例模式

 懒汉式实现就是为了解决饿汉式在类加载时就创建类实例的缺点,采用了延迟加载思想,在需要用到的时候再创建。

线程不安全实现(一)
/**
 * Lazy Singleton, Thread Unsafe-1
 * @author Horizon
 *
 * @Date 2019年6月4日下午8:29:20
 */
public class Singleton {

    // 静态变量
    private static Singleton instance;
    
    // 私有构造
    private Singleton() {}
    
    // 静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) { // 1
            instance = new Singleton(); // 2
        }
        return instance; // 3
    }
    
}

 以上的实现代码在单线程运行下可以实现单例,但如果是在多线程环境下运行的话,就可能会创建出两个以上实例的情况。  
 比如:假设有2个线程同时执行getInstance方法,Thread1先进行第1步的判断为空,进入if语句内部,还未执行第2步时,执行权给到了Thread2,此时Thread2进行if判断还是为空,执行第2、3步后返回了实例,此时切回Thread1继续执行第2、3步返回实例,这样两个线程就返回了两个不同的实例,违背了单例模式的定义。

线程不安全实现(二)
/**
 * Lazy Singleton Thread Unsafe-2
 * @author Horizon
 *
 * @Date 2019年6月4日下午8:45:34
 */
public class Singleton {

    // 静态变量
    private static Singleton instance;
    
    // 私有构造
    private Singleton() {}
    
    // 静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) { // 1
            synchronized (Singleton.class) { // 2
                if (instance == null) { // 3
                    instance = new Singleton(); // 4
                }
            }
        }
        return instance; // 5
    }
    
}

 以上实现代码采用了“双重检锁”,第1步的存在是为了当实例被创建后,再次访问时避免加锁操作;当有多个线程通过第1步,对第2步锁的竞争只有一个线程能获得锁,其余的都将阻塞在第2步,第3步的存在是为了让被阻塞在第2步的线程被唤醒后进行第二次判断,如果前面已有线程执行了第4步创建了实例,那么后续的线程就不应该执行第4步了。

双重检锁线程安全实现

 以上的实现代码看似已经没有问题了,但其实还存在一个比较隐蔽的问题,这涉及到JVM指令重排序问题,第4步的执行在JVM层面被分为3个步骤完成:

  1. 为实例对象分配内存空间
  2. 执行构造,初始化等操作
  3. 变量引用指向实例对象

 以上对象创建的第2、3步可能会被重排序,即先执行3再执行2,那么当线程执行了3后,instance已经有了引用,此时另一个线程来到第一个if判断后不为空就返回了instance,此时的instance是未执行第2步的操作的,而当原来线程返回执行第2步后返回时,就出现了两个不同的实例了。  
 这就是为什么变量要使用volatile修饰的原因了,因为volatile修饰的变量可以禁止指令的重排,这样就不会在对象还未实例化完成就被引用了。  
 双重检锁线程安全代码实现:

/**
 * Lazy Singleton Thread Safe
 * @author Horizon
 *
 * @Date 2019年6月4日下午9:21:34
 */
public class Singleton {

    // 静态变量,volatile修饰
    private static volatile Singleton instance;
    
    // 私有构造
    private Singleton() {}
    
    // 静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) { 
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance; 
    }
    
}
枚举实现单例
/**
 * Enum Singleton Thread Safe
 * @author Horizon
 *
 * @Date 2019年6月4日下午9:46:35
 */
public enum Singleton {
    INSTANCE;
}

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

静态内部类实现单例
/**
 * Static Inner class, Thread Safe
 * @author Horizon
 *
 * @Date 2019年6月4日下午9:51:49
 */
public class Singleton {

    // 私有构造
    private Singleton() {}
    
    // 静态内部类
    private static class SingletonHelp {
        // 单例实例
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        // 此方法被调用时才会加载Handler类并创建单例实例
        return SingletonHelp.INSTANCE;
    }
    
}

 这种实现方式结合了“饿汉”使用类加载时创建实例和“懒汉”延迟加载的特点,在加载单例类时,静态内部类并不会加载,因此不会创建单例实例,此为延迟加载的特点,等到getInstance方法被调用时,静态内部类被加载并创建了单例实例,此为类加载时创建实例的特点。

为什么反序列化会破坏单例模式?

 反序列化会通过反射调用单例类的构造函数,这样就破坏了单例类私有构造函数的作用了,可以通过在单例类中自定义readResolve方法返回单例实例,这样在反序列化时会调用自定义的readResolve方法,关于为什么会调用,可以去看看ObjectInputStream的readOrdinaryObject方法的实现。

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;

    // 私有构造
    private Singleton() {}
    
    // 静态内部类
    private static class SingletonHelp {
        // 单例实例
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        // 此方法被调用时才会加载Handler类并创建单例实例
        return SingletonHelp.INSTANCE;
    }

    // 此方法防止反序列化破坏单例模式,可将此方法注释测试看看
    private Object readResolve() {
        return SingletonHelp.INSTANCE;
    }
    
    public static void main(String[] args) throws Exception {
        Singleton instance = getInstance();
        System.out.println(instance);
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\tmp.out"));
                ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\tmp.out"))) {
            out.writeObject(instance);
            Singleton s = (Singleton) in.readObject();
            System.out.println(s);
        }
    }
    
}