单例模式——唯一实例与中心化


Demo 地址: https://github.com/ooblee/HelloDesignPattern

1. 定义

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

一句话总结,就是唯一实例,中心化

现实世界的模型:

  • 日本只有一个天皇。
  • 一个国家只有一个央行。

单例模式需要满足以下的特点:

  • 单例类全局只有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一个实例。

2. 设计

单例类的类图可以简单表示为:单例模式

如果正确地设计单列,需要关注两个点:

  • 线程安全。
  • 性能。

因为单例类需要自己实例化自身,并且要确保在多线程环境下不会产生多个实例,而且并发下性能到达最优。

根据实例化的时机,分为两种:

  • 饿汉模式,类加载的时候就初始化。

    扫描二维码关注公众号,回复: 10590283 查看本文章
  • 懒汉模式,延迟初始化。

2.1. 懒汉:错误方式

为了实现懒汉,延迟初始化单例,我们把实例化的过程推迟到第一次访问单例上。于是就有了这样的代码

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

看上去很美好,实则有一个很大的隐患,多线程调用 getInstance 方法会产生多实例。

简单分析一下,假设 A 线程和 B 线程同时调用

  • A 线程执行 instance = new Singleton(); ,语句还没结束, instance 还为 null
  • 这时候 B 线程进入语句 if (instance == null) ,条件成立,也进入 instance = new Singleton();
  • 两个线程均建立了实例

为了确保能够延迟初始化,并且做到线程安全,下面会开始介绍

2.2. 懒汉:方法直接加同步(不要用)

最简单粗暴的方式,获取实例的方法直接上锁:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    	if (instance == null) {  
        	instance = new Singleton();  
    	}  
    	return instance;  
    }  
}  

确实线程安全了。

但是 synchronized 是互斥锁,悲观锁,getInstance 被调用频繁的情况下性能低下。

这样吧,我们把锁的粒度

2.3. 懒汉:双重检查锁定(推荐,记得 volatile)

双重检查锁定,本质是对锁的一个粒度优化。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    	if (singleton == null) {  
        	synchronized (Singleton.class) {  
        		if (singleton == null) {  
            		singleton = new Singleton();  
        		}  
        	}  
    	}  
    	return singleton;  
    }  
} 

只有判断实例为空,才会进入同步代码。实例存在的话就直接返回了。

所以只有在创建的那一刹那,并且有多线程并发,会有一个互斥等待的过程。

因为进入同步代码,有可能其他线程已经实例化完毕,所以还要再检查一下是否已经实例化(判空),这就是双重检查。

因为 JVM 的指令优化,指令重排序现象会导致对象延迟初始化,所以其他线程读到实例不为空的时候,可能还没初始化。

所以需要加个 volatile 关键字,禁止重排序优化。

2.4. 懒汉:静态内部类(非常推荐)

这是一个比较机智的做法,利用 JVM 类加载机制,来延迟初始化对象实例

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

什么时候会进行类加载?

这里的静态内部类,外部的 Singleton 加载的时候并不会引起它的加载。因为虽然是它的内部静态类,但编译成字节码文件后是两个单独的类。

在调用 getInstance 方法后,静态内部类被主动调用,触发类加载流程。

而类加载流程是天然同步的,我们可以从源码上看到:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
 {
	synchronized (getClassLoadingLock(name)) {
		...
		return c;
	}
}

我们也就不需要再进行加锁了。

2.5. 饿汉:静态工厂方法

在单例类加载的时候,马上实例化单例对象。

public class Singleton {  
    private Singleton() {}  
    private static final Singleton singleton = new Singleton();  
    public static Singleton getInstance() {  
        return singleton;  
    }  
}  

始终是线程安全的。

缺点是,及时没有调用该单例,也会在类使用的时候一开始就创建好了,在一些对性能要求高的场景会消耗性能。(这个在客户端场景比较常见)

2.6. 枚举实现

public enum Singleton {
 	INSTANCE;
 	private Singleton() {
 	}
}

创建枚举默认是线程安全的

3. 应用

什么时候需要使用单例?

控制实例的访问,所有的访问必须在单一实例上进行。

控制资源的使用,和线程池、连接池等配合使用,资源型单例,避免创建多个池导致资源的浪费。

控制对象的创建,不需要重复创建的对象,和工厂模式配合,比如实现工厂实例的唯一。

在客户端环境,比如 Android 开发,因为对应用启动的性能要求高,不希望应用一加载马上进行单例实例化,所以对懒汉模式应用较多。

3.1. JDK:Runtime

Runtime 可以获取 JVM 运行环境的信息。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
	
	...
}

4. 特点

4.1. 优势

  • 中心化:控制所有的访问在唯一实例上进行
  • 性能优化:避免频繁创建和销毁对象带来的性能损耗
  • 内存优化:避免可共享资源的重复创建

4.2. 缺点

  • 职责膨胀:如果承担的职责过多,违背单一职责原则

    优化思路

    把职责再重新剥离出去,或者与其他设计模式一起使用,单例仅作为入口

  • 内存泄漏:如果成员变量引用了本来该释放的对象,引起泄漏,进而导致 OOM(内存溢出)

    优化思路

    第一,注意引用对象的生命周期

    第二,如果确定要引用,需要有良好的内存释放机制。比如该成员变量为缓存池,考虑使用弱引用或者软引用,在虚拟机垃圾回收阶段或者内存紧张的时候进行对象的回收

4.3. 注意事项

  • 不要滥用:实例和进程生命周期一致,长期不使用白白占用内存
  • 不要反射:发射生成实例,会破坏单例的设计。(之前有遇到坑,继承的第三方服务反射一个单例类导致不唯一)
  • 注意引用:成员变量尽量不去持有生命周期短的对象,非要持久需要注意释放机制
  • 注意性能:根据业务来决定是否需要延迟初始化(用来抉择懒汉还是饿汉)
  • 注意线程安全:使用懒汉模式,注意线程安全避免产生多个实例
发布了61 篇原创文章 · 获赞 43 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/firefile/article/details/90311767