定义
属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。即:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例(所以构造方法私有)。
- 单例类必须给所有其他对象提供这一实例(静态访问方法)。
主要解决一个全局使用的类频繁地创建与销毁这一问题。
优缺点
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例,还可以减少系统的性能开销。
- 避免对资源的多重占用(比如写文件操作)。
缺点:
单例模式没有抽象层,没有接口,不能继承(构造方法私有),扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
单例类的职责过重,在一定程度上违背了“单一职责原则”。
滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;又比如:在多个线程中操作单例类的成员时,但单例中并没有对该成员进行线程互斥处理。
应用场景
- (状态化)WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
- 要求生产唯一序列号。
- (无状态化)提供工具性质的功能。
- 。。。。。。。。
实现
饿汉式
public class Hungry {
//饿汉式就是一上来就先把对象加载
private final static Hungry HUNGRY = new Hungry();
//私有构造器
private Hungry(){ }
//暴露给外部获取单例实例的方法
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。
优点:只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。
缺点:即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了,如果类里面还存在其他变量(如一个很大的数组)就更浪费内存了。
为了解决这一缺点,有了懒汉式实现
懒汉式
如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建。所以懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan;//区别于饿汉式,先不创建实例
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();//需要的时候再创建
}
return lazyMan;
}
}
但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例。
比如启多个线程时,正常情况下只会有一个实例(左),那么"创建"两个字只会打印一次,但多线程下却有多个实例(右):
public class LazyMan {
private static LazyMan lazyMan;//区别于饿汉式,先不创建实例
private LazyMan(){
}
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();//需要的时候再创建
System.out.println("创建");
}
return lazyMan;
}
public static void main(String[] args){
for(int i=0; i<20; i++){
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
因此需要加锁解决线程同步问题,实现如下:
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){}
public static synchronized LazyMan getInstance(){ //加锁
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
但这种方式效率低,第一次加载需要实例化,反应稍慢。每次调用getInstance方法都会进行同步,消耗不必要的资源。为了提高资源利用率,有了双重检查单例。
双重检查单例(DCL懒汉式)
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){}
public static LazyMan getInstance(){
if(lazyMan == null){//避免了不必要的同步
synchronized (LazyMan.class){
if(lazyMan == null){//在null的情况下再去创建实例
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
第一层判断的主要避免了不必要的同步,第二层判断是为了在null的情况下再去创建实例。
但这种双重检测也存在问题,因为lazyMan = new LazyMan();不是原子操作。
lazyMan = new LazyMan();分三步:
- 给LazyMan实例分配内存,将函数压栈,并且申明变量类型。
- 初始化构造函数以及里面的字段,在堆内存开辟空间。
- 将lazyMan对象指向分配的内存空间。
但是以上三步并不一定按照顺序执行,因为"指令重排",会有问题,比如:
A线程按照132执行,假设刚执行完第3步,layzyMan已经分配了内存空间,但并未初始化。此时,线程B获取实例的时候,因为A的操作,B执行时会认为第一层的lazyMan != null,而直接return lazyMan,而此时lazyMan还未完成构造,是有问题的。
为了解决这一问题,必须借助volatile(保证不发生指令重排,不保证原子性):
public class LazyMan {
private volatile static LazyMan lazyMan; //volatile
private LazyMan(){}
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
关于static关键字的作用:如果去掉关键字的static,那么getInstance的关键字也要被去掉,那么调用时就不能通过Single.getInstance调用,就必须new Single(),那么单例也就失去作用了,因为不同线程会自己new一个Single
静态内部类
此外还可以通过静态内部类的方式去实现。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
其利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
上述的双重检查单例是可以通过反射来破坏的!!!,如:
其结果不一样,说明存在两个实例,单例被破坏:
为了解决这一问题,需要用到枚举(反射不能破坏枚举)。
枚举
public enum EnumSingle {
INSTANCE;
public void fuckUSA(){
System.out.println("freedom");
}
}
使用 EnumSingle.INSTANCE.fuckUSA()即可执行对应方法,相对于其他单例来说枚举写法最简单,并且任何情况下都是单例的
容器单例
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManager() {
}
public static void putObject(String key, String instance){
if(!objMap.containsKey(key)){
objMap.put(key, instance);
}
}
public static Object getObject(String key){
return objMap.get(key);
}
}
在开始的时候将单例类型注入到一个容器之中,也就是单例SingletonManager,在使用的时候再根据key值获取对应的实例,这种方式可以使我们很方便的管理很多单例对象,也对用户隐藏了具体实现类,降低了耦合度;但是为了避免造成内存泄漏,一般在生命周期销毁的时候也要去销毁它。