单例模式

673 阅读4分钟

定义

  • 创建型模式(其它的有结构型、行为型)
  • 一个单一的类,自己负责创建自己的对象,同时确保只有单个对象被创建,并提供一个访问其唯一对象的方式。

解决的问题/优点

  • 频繁使用的对象,节约创建对象的时间
  • 因为没有频繁创建对象,所以 GC(垃圾回收)的压力也更小

缺点

  • 简单的单例模式,开发简单,复杂情况要考虑线程安全等并发问题
  • 没有接口,不能继承,与单一职责原则冲突(一个类只关心内部逻辑,不关心如何实例化)

关键代码

  • 私有静态属性保存单例:private static Singleton instance;
  • 私有构造函数防止外部创建:private Singleton();
  • 私有静态的获取唯一实例的方法:在获取时判断是否存在,存在则返回实例,不存在则创建;考虑到并发时,多线程同时访问会创建多个实例,在实现时要有锁机制。public static Singleton getInstance();

实现方式

实现时注意:

  • 线程安全
  • 延迟加载(性能考虑)
  • 代码安全

实现方式主要有以下几种

  • 懒汉式(线程不安全)
  • 懒汉式(线程安全)
  • 饿汉式
  • 双重校验锁(DCL,Double-Checking Clocking)
  • 静态内部类
  • 枚举
  • 登记式

懒汉式(线程不安全)

优点:

  • 懒加载
  • 易实现

缺点:

  • 线程不安全(不支持多线程,多个线程同时访问 getInstance() 方法时,会有可能创建多个实例,所以严格意义上不是单例模式)
class Singleton {
    private static Singleton instance;
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
      
        return instance;
    }
}

懒汉式(线程安全)

优点:

  • 懒加载
  • 易实现
  • 线程安全

缺点:

  • 效率低(synchronized 同步加锁影响效率)
class Singleton {
    private static Singleton instance;
  
    private Singleton() {}
  
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
      
        return instance;
    }
}

饿汉式

优点:

  • 线程安全
  • 易实现
  • 没有加锁,执行效率高

缺点:

  • 非懒加载
  • 类加载时就初始化,浪费内存(基于 classloader 机制)
class Singleton {
    private static Singleton instance = new Singleton();
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        return instance;
    }
}

双重校验锁(DCL,Double-Checking Locking)

优点:

  • 懒加载
  • 线程安全
  • 双锁机制,多线程下能保持高性能

缺点:

  • 实现较复杂
class Singleton {
    private volatile static Singleton instance;
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
      
        return instance;
    }
}

为什么有两重 instance 是否为 null 的判断呢? 当 instance 为 null 并且同时有两个线程调用 getInstance() 方法时,它们将都可以通过第一重 instance == null 的判断。然后由于 lock 机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有第二重的 instance 是否为 null 的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。

静态内部类

优点:

  • 懒加载
  • 线程安全
  • 实现难度一般

和饿汉式一样利用了 classloader 机制,不同的是调用的时候才加载,可以延迟加载

class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚举

优点:

  • 线程安全
  • 易实现,实现简洁
  • 自动支持序列化

缺点:

  • 非懒加载

没有广泛采用

public enum Singleton() {
    INSTANCE;
}

登记式

map 登记式单例模式是对一组单例模式进行的维护, 保证 map 中的对象是同一份,Spring 中使用的就是类似的模式:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class RegisterSingleton {
    /** * 登记式单例模式, 保证map中的对象是同一份 */
    private static Map<String, Object> map;

    static {
        map = new ConcurrentHashMap<>();
        map.put(RegisterSingleton.class.getName(), new RegisterSingleton());
    }

    private RegisterSingleton() {
        System.out.println("this Constructor is called");
    }

    public static Object getInstance(String name) {
        if (name == null) {
            name = RegisterSingleton.class.getName();
        }
        if (map.get(name) == null) {
            try {
                map.put(name, Class.forName(name).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
}

总结

不用懒汉式,使用饿汉式,明确懒加载使用静态内部类(登记式),如果涉及序列化,尝试枚举,其它特殊要求使用 DCL。

应用举例

  • 文件系统:多进程/线程同时操作一个文件,需保证文件内容的一致性,只能有一个文件实例。
  • 设备管理器:一个电脑不能同时有两个打印机实例,否则打印时同一文件将在多台打印机上打印。
  • 数据库连接池
  • 网络连接池

扩展

有限的单例模式 将多个实例对象保存在 ArrayList 中,需要时随机获取。

package com.xuuu.jdp.multiton;

import java.util.ArrayList;
import java.util.List;

public class Multiton {

    private static final int NUM = 4;

    private static List<Multiton> instances = new ArrayList<>();

    private Multiton(int i) {}

    static {
        for (int i = 0; i < NUM; i++) {
            instances.add(new Multiton(i));
        }
    }

    public static Multiton getRandomInstance() {
        int value = (int) (Math.random() * NUM);
        return instances.get(value);
    }

}

反射机制破解单例模式(枚举除外)

public class BreakSingleton{
    public static void main(String[] args) throw Exception{
        Class clazz = Class.forName("Singleton");
        Constructor c = clazz.getDeclaredConstructor(null);

        c.setAccessible(true);

        Singleton s1 = c.newInstance();
        Singleton s2 = c.newInstance();
        //通过反射,得到的两个不同对象
        System.out.println(s1);
        System.out.println(s2);
    }
}

如何避免以上的漏洞:

class Singleton{
    private static final Singleton singleton = new Singleton(); 
    
    private Singleton() {
        //在构造器中加个逻辑判断,多次调用抛出异常
        if(instance!= null){
            throw new RuntimeException()
        }
    }

    public static Singleton getInstance(){
        return singleton;
    }
}

参考