单例模式(Singleton)

单例模式的介绍、使用场景、七种写法及测试

概述

单例模式就是:确保一个类只有一个实例,并提供该实例的全局访问点。

使用场景

  1. 平常写代码的时候全局属性的保存(有状态的工具类对象,如使用 excel 导出带下拉的时候,需要一个 handler 缓存下拉值,这个下拉值需要全局使用)
  2. 多个模块使用同一个数据源连接对象
  3. 多线程的线程池也是被设计为单例,方便对池中现成进行控制

适用场景

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象耗时过多或耗资源过多,但是又会经常用到的对象
  3. 方便资源相互通信的环境

实现方式

是否必须用单例?

在不需要维持任何状态下,仅仅用于全局访问,这个使用使用静态类的方式更加方便;但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式,以下这种方式在我们开发中非常常见。

public class Singleton00 {

   public static Map<String,String> cache  = new ConcurrentHashMap<>();

}

单例的七种实现方式

并发环境测试代码

因为涉及到判断我们使用的单例模式是否是线程安全的,所以我们需要一个并发环境来测试,如下是具体代码,整体思路是,设置一个计数器,然后启动一个线程,计数器减一,当计数器为0时,所有线程同时启动,通过 getInstance 方法拿对象

public class Main {
    public static void main(String[] args) throws InterruptedException {

        int threadNumber = 100;
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(threadNumber);
        ExecutorService executorService = Executors.newFixedThreadPool(threadNumber);
        for (int i = 0; i < threadNumber; i++) {
            executorService.submit(() -> {
                try {
                    // 先阻塞这别让这个线程跑起来
                    start.await();
                    // 创建实例
                    Singleton01.getInstance();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 一个线程跑完 end计数器-1
                    end.countDown();
                }
            });
        }

        // start-1 所有线程启动,模拟并发
        start.countDown();
        // 阻塞直到执行完毕
        end.await();
        executorService.shutdown();

    }
}

懒汉式(线程不安全)

优点:延迟实例化,用到对象的时候再new

缺点:多线程环境下,线程不安全,多个线程同时进入 getInstance 方法,并且都判断自己为 null ,所以就会出现多次 new Singleton01() 的情况 【不推荐】

public class Singleton01 {

    private static Singleton01 instance;

    private Singleton01() {
        System.out.println("Singleton01实例化");
    }

    public static Singleton01 getInstance() {
        if (null != instance) {
            return instance;
        }
        instance = new Singleton01();
        return instance;
    }
}

并发环境下获取实例测试,会打印多次,也就是会构造多个对象

Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化

懒汉式(线程安全)

既然上述 getInstance() 的方法不安全,那就加个锁好了,别让线程都进来,没抢到的等着就行

但是,每次访问 getInstance() 都需要锁占用导致资源浪费 【不推荐】

public class Singleton02 {

    private static Singleton02 instance;

    private Singleton02() {
        System.out.println("Singleton02实例化");
    }

    public static synchronized Singleton02 getInstance() {
        if (null != instance) {
            return instance;
        }
        instance = new Singleton02();
        return instance;
    }
}

并发测试:

Singleton02实例化

饿汉式(线程安全)

上述线程不安全的问题主要是由于实例化了多次,我们这次类加载的时候就直接实例化,从而避免了实例化多次的情况发生,但是也不能节约资源了【不推荐】

public class Singleton03 {
    private static final Singleton03 INSTANCE = new Singleton03();

    private Singleton03() {
        System.out.println("Singleton03实例化");
    }

    public static Singleton03 getInstance() {
        return INSTANCE;
    }
}

使用类的内部类(线程安全)

因为用的时候才会加载内部类,jvm 可以保证多线程下类的<clinit>只会执行一次,其他线程都会阻塞等待

既实现了延迟加载,又实现了线程安全【推荐使用】

public class Singleton04 {

    private static class SingletonHolder {
        private static final Singleton04 INSTANCE = new Singleton04();
    }

    private Singleton04() {
        System.out.println("Singleton04实例化");
    }

    public static Singleton04 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

双重校验锁(线程安全)

双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。也满足了懒加载

这里为什么采用 volatile 关键字修饰实例对象?

因为instance = new Singleton05();这段代码其实分三步执行:

  1. 为 instance 分配内存空间
  2. 初始化 instance
  3. 将 instance 指向分配的内存地址

由于 JVM 具有指令重排的特性,执行顺序也有可能变为1 -> 3 -> 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下,会出现一种情况,一个线程 A 执行了 1 和 3,也就是分配了内存空间,还把这个对象指向了那个内存空间了,但是就是还没初始化呢,另一个线程 B 进入 getInstance 方法,一看对象不是空的,就直接返回了,但是此时返回这个是没有初始化的对象。

volatile 关键字可以禁止 JVM 的指令重排功能,保证多线程环境下也可以正常运行。

public class Singleton05 {
    private static volatile Singleton05 instance;

    private Singleton05() {
        System.out.println("Singleton05实例化");
    }

    public static Singleton05 getInstance() {
        if (null != instance) {
            return instance;
        }
        synchronized (Singleton05.class) {
            if (null == instance) {
                instance = new Singleton05();
            }
        }
        return instance;
    }
}

CAS「AtomicReference」(线程安全)

java 并发库提供了很多原子类来支持并发访问的数据安全性;AtomicIntegerAtomicBooleanAtomicLongAtomicReferenceAtomicReference<V> 可以封装引用一个V实例,支持并发访问。

使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。

当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。

public class Singleton06 {

    private static final AtomicReference<Singleton06> INSTANCE = new AtomicReference<>();

    private static Singleton06 instance;

    private Singleton06() {
        System.out.println("Singleton06实例化");
    }

    public static final Singleton06 getInstance() {
        for (; ;) {
            Singleton06 instance = INSTANCE.get();
            if (null != instance) {
                return instance;
            }
            INSTANCE.compareAndSet(null, new Singleton06());
            return INSTANCE.get();
        }
    }
}

枚举(线程安全)

枚举有两种方式,一种是新建一个类,就是来做单例的;

还有一种是对已有类改造为单例模式的场景。

枚举实现单例好处:这种方式解决了最主要的;线程安全、自由串行化、单一实例。

可以防止反射攻击

  1. 新建一个类做单例

    public enum Singleton071 {
        /** 实例 */
        INSTANCE;
    
        public void businessMethod() {
            System.out.println("业务方法~");
        }
    
        public static void main(String[] args) {
            Singleton071 instance = Singleton071.INSTANCE;
            Singleton071 instance1 = Singleton071.INSTANCE;
            // true
            System.out.println(instance1 == instance);
        }
    }
  2. 对已有类改造为单例

    public class Singleton072 {
    
        private Singleton072() {
            System.out.println("Singleton072实例化");
        }
    
        private enum SingletonEnum {
            /** 枚举对象 */
            INSTANCE;
    
            private final Singleton072 instance;
    
            SingletonEnum() {
                instance = new Singleton072();
            }
    
            private Singleton072 getInstance() {
                return instance;
            }
        }
    
        public static Singleton072 getInstance() {
            return SingletonEnum.INSTANCE.getInstance();
        }
    }

单例模式(Singleton)
https://www.powercheng.fun/articles/2c8b996c/
作者
powercheng
发布于
2022年3月10日
许可协议