6种单例模式实现以及测试

55 阅读1分钟

懒汉模式(线程不安全)

描述 :只有在需要实例化对象时才会去实例化,但是由于多线程环境下会出现多个线程同时获取到 singleton_1==null 的情况。此时多个线程会出现重复实例化,并持有不同实例对象。所以一般只在单线程环境下使用。

public class LazySingleton {
    private static LazySingleton singleton = null;
​
    private  LazySingleton() {
        System.out.println(this);
    }
​
    public static LazySingleton getInstance(){
        if (singleton == null){
            singleton = new LazySingleton();
        }
        return singleton;
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(LazySingleton::getInstance).start();
        }
    }
}

懒汉模式(线程安全)

描述:为了解决懒汉模式下多个线程同时实例化的情况,可以在获取实例的方法加上 synchronized 关键字。但是会出现一个线程持有锁后,其它线程无法访问到该方法。其它线程也只有在前一个线程释放锁后才能去竞争这个锁。竞争到锁的线程可以访问该方法。也就造成了多线程只能串行去访问该方法。

严重影响了执行效率。

public class SyncLazySingleton {
    private static SyncLazySingleton singleton = null;
​
    private  SyncLazySingleton() {
        System.out.println(this);
    }
​
    public static synchronized SyncLazySingleton getInstance(){
        if (singleton == null){
            singleton = new SyncLazySingleton();
        }
        return singleton;
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(SyncLazySingleton::getInstance).start();
        }
    }
}

饿汉模式

描述:通过 finalstatic 修饰类对象变量,确保 INSTANCE 在类被装载时就初始化,并无法被修改。不需要加锁就能保证多线程环境下只会有一个实例。但是类加载时就初始化,浪费内存。

public class HungerSingleton {
    private static final HungerSingleton INSTANCE = new HungerSingleton();
​
    private HungerSingleton() {
        System.out.println(this);
    }
​
    public static HungerSingleton getInstance(){
        return INSTANCE;
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(HungerSingleton::getInstance).start();
        }
    }
}

Java标准库有一些类就是单例,例如Runtime这个类:

Runtime runtime = Runtime.getRuntime();

双检锁

描述:双检锁是对加锁后的懒汉模式改进。加入双重检查机制,可以在第一检查 singleton_4 == null 时才考虑后面操作加锁,避免了每个线程进入该方法都要持有锁的情况出现。在线程持有锁后为什么要再次检查对象 singleton_4 == null 呢?主要还是为了避免第一次检查时多个线程都走到了第一次检查 singleton_4 == null 的情况,但是有一个线程完成了实例化后,其他线程可以再次判断。

volatile 关键字对类对象变量的修饰是为了阻止指令重排序的情况出现。因为实例化对象的过程其实有步骤:

1.分配内存

2.初始化对象

3.将对象指向刚才分配的内存空间

其中第二步和第三步可能会出现指令重排序的情况,这就导致了对象变量指向了一个未初始化的内存空间。这种对象变量如果被其他线程获取后使用会出现异常。

public class DoubleCheckLockSingleton {
    private static volatile DoubleCheckLockSingleton singleton = null;
​
    private DoubleCheckLockSingleton() {
        System.out.println(this);
    }
​
    public static DoubleCheckLockSingleton getInstance(){
        if (singleton==null){
            synchronized(DoubleCheckLockSingleton.class){
                if (singleton == null){
                    singleton = new DoubleCheckLockSingleton();
                }
            }
        }
        return singleton;
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(DoubleCheckLockSingleton::getInstance).start();
        }
    }
}

内部静态类

描述:和饿汉模式一样都是利用ClassLoader类加载机制来保证初始化时只有一个线程。不一样的是,这里的内部静态类只有在被调用时才会被装载。相比饿汉模式会节约内存。

public class StaticInnerClassSingleton {
    private static StaticInnerClassSingleton singleton = null;
​
    private StaticInnerClassSingleton() {
        System.out.println(this);
    }
​
    private static class StaticInnerClassSingletonHolder{
        private static final StaticInnerClassSingleton SINGLETON = new StaticInnerClassSingleton();
    }
​
    public static StaticInnerClassSingleton getInstance(){
        return StaticInnerClassSingletonHolder.SINGLETON;
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(StaticInnerClassSingleton::getInstance).start();
        }
    }
}

以上5中单例模式无法避免通过反射来破坏单例;可以通过枚举来防止反射破坏单例;

枚举

描述:这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

public enum EnumSingleton {
    SINGLETON;
​
    EnumSingleton() {
        System.out.println(this);
    }
​
    public static void main(String[] args) {
        //验证饿汉模式的线程安全性
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                EnumSingleton singleton = EnumSingleton.SINGLETON;
                System.out.println(singleton);
            }).start();
        }
    }
}

约定大于配置: Spring中的单例对象不是按照以上这些单例模式实现语法来实现的,因为这样会很麻烦,使用者也不希望自己写的每个类写成单例都这么麻烦。所以Spring一般是通过 BeanFactory 实例化后,放入 ConcurrentHashMap 进行管理,使用者一般不会去 new 一个新的实例;

关于单例优缺点的思考

优点:

  • 一个对象在内存中只有一个实例,减少了内存的开销。
  • 可以避免资源的多重占用(如:写文件操作)。

缺点:

  • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 要求生产唯一序列号。
  • WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。