单例及单例模式概念
一个类只允许创建一个实例。这种设计模式就是单例模式
实现单例模式需要考虑什么?
- 构造函数私有,避免外部通过new来创建实例
- 考虑是否需要延时加载
- 考虑线程安全
单例模式的几种实现方式
1. 饿汉式:在类加载的时候,就已经初始化实例了。
publicclassHungry { // 一启动就被加载,还未使用就占用内存,造成资源浪费privatestaticfinalHungryINSTANCE=newHungry(); // 构造方法私有,外部无法进行实例化privateHungry() {} // 获取实例对象publicstaticHungrygetInstance() { returnINSTANCE; } }
2. 懒汉式:第一次使用时才进行实例化
publicclassLasy { // 启动时不加载,需要时再进行实例化privatestaticLasyINSTANCE; // 构造方法私有,外部无法进行实例化privateLasy() {} publicstaticLasygetInstance() { if (null==INSTANCE) { INSTANCE=newLasy(); } returnINSTANCE; } }
但是懒汉式并不能保证单例,在多线程并发情况下,会多次调用构造方法进行实例化,示例如下:
publicclassLasy { // 启动时不加载,需要时再进行实例化privatestaticLasyINSTANCE; // 构造方法私有,外部无法进行实例化privateLasy() { System.out.println(Thread.currentThread().getName()); } publicstaticLasygetInstance() { if (null==INSTANCE) { INSTANCE=newLasy(); } returnINSTANCE; } publicstaticvoidmain(String[] args) { for (inti=0; i<10; i++) { newThread(Lasy::getInstance).start(); } } }
输出结果:
2.1 使用同步锁synchronized
//如果使用synchronized关键字,这种方式在并发下并发量等同于1,如果频繁使用该对象,会大大降低性能publicstaticsynchronizedLasygetInstance() { if (null==INSTANCE) { INSTANCE=newLasy(); } returnINSTANCE; }
2.2 双重检测(DCL懒汉式单例)
publicstaticLasygetInstance() { if (null==INSTANCE) { synchronized (Lasy.class) { if (null==INSTANCE) { INSTANCE=newLasy(); } } } returnINSTANCE; } // INSTANCE = new Lasy();/** 以上操作并不是原子性操作,包括以下三个步骤:* 1. 分配内存空间* 2. 执行构造方法进行初始化* 3. 将对象指向这个空间*//** 在多线程情况下可能出现指令重排,每个线程执行INSTANCE = new Lasy()这个操作的顺序可能不一致,那么就有可能出现线程A先执行了第一步和第三步,此时还未进行初始化,线程B执行判断语句null == INSTANCE时为false,将直接返回线程A未进行初始化的INSTANCE对象,所以要避免指令重排,使用volatile关键字。*/privatestaticvolatileLasyINSTANCE; publicstaticLasygetInstance() { if (null==INSTANCE) { // 对象未被实例化,等待类对象锁synchronized (Lasy.class) { // 进入对象锁,其他线程等待,再次判断是否被实例化if (null==INSTANCE) { INSTANCE=newLasy(); } } } returnINSTANCE; }
饿汉式和懒汉式比较:
①由于饿汉式是在类加载的时候进行实例化,线程安全;懒汉式是在第一次使用的时候进行实例化,此时可能多线程会同时进行实例化,所以线程不安全;
②饿汉式不支持延时加载,如果占用资源较多或者耗时较长,可能会影响启动时间;懒汉式支持延时加载,但是需要使用同步锁,如果这个类被频繁使用,可能导致性能问题。(相比较更加倾向于饿汉式单例,在启动时进行加载,如果遇到性能或者资源问题会提前暴露,相较于正在运行中出现问题更加可控)
3. 静态内部类
publicclassTest { privateTest() {} privatestaticclassInnerTest{ // final修饰变量是不可变的,初始化对象之后便不会再指向另一个对象privatestaticfinalTestINSTANCE=newTest(); } publicstaticTestgetInstance() { returnInnerTest.INSTANCE; } }
但是在反射机制下,所有的私有方法都能被破解,如下实例表示通过静态内部类获得的实例对象和通过反射获得的holder实例对象并不是同一个,仍然破坏了单例
publicstaticvoidmain(String[] args) throwsException { // 获取private的构造方法Constructor<Test>constructor=Test.class.getDeclaredConstructor(null); // 无视私有构造器constructor.setAccessible(true); Testtest=constructor.newInstance(); System.out.println(Test.getInstance().equals(test)); } 输出结果:false
通过反射的源码可以看到,无法反射式创建枚举对象,所以我们可以考虑使用枚举来实现单例
4. 枚举
publicenumSingleEnum { INSTANCE; }
以下示例可见确实无法反射式创建枚举对象,当使用反射创建枚举对象时,会抛出异常信息Cannot reflectively create enum objects
publicstaticvoidmain(String[] args) throwsException { // 获取private的构造方法(实际上枚举类的构造方法带有两个参数,并不是我们从源码看到的无参构造,需要使用源码工具来查看)Constructor<SingleEnum>constructor=SingleEnum.class.getDeclaredConstructor(String.class, int.class); // 无视私有构造器constructor.setAccessible(true); SingleEnumsingle=constructor.newInstance(); System.out.println(SingleEnum.INSTANCE.equals(single)); }
输出: