根据B站尚学堂视频整理:https://www.bilibili.com/video/BV1F54y1R7L1?p=1
参考菜鸟教程:https://www.runoob.com/design-pattern/singleton-pattern.html
核心作用: 保证一个类只有一个实例,并且提供了一个访问该实例的全局访问点。
常见应用场景:
windows的任务管理器 就是一个单例模式。
winwods的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护者仅有的一个实例。
网站的计数器,一般也是采用单例模式实现,否则难以同步。
在Spring的Bean默认就是单例的,这样做的优点是Spring容器可以管理。
Spring MVC框架中,控制器对象也是单例。
单例模式的优点:
由于单例模式只生成一个实例,减少了系统性能的开销。当一个对象的生产需要比较多的资源时,可以通过在应用启动时直接生产一个单例对象,然后永久驻留内存的方式来解决。
单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
常见的五种单例模式:
主要
懒汉式(线程安全,调用效率不高。可以延迟加载)
饿汉式(线程安全,调用效率高。不能延迟加载)
其他
延迟加载(lazy load)也称为懒加载。
简单来说,就是只有在使用的时候,才去调用或加载。
是延迟加载。
代码实现:
//懒汉式 单例模式
public class SingletonDemo2 {
private static SingletonDemo2 instance;
//构造器私有化。这样该类就不会被实例化
private SingletonDemo2(){
}
//方法同步,调用效率低
public static synchronized SingletonDemo2 getInstance(){
if (instance == null){
instance = new SingletonDemo2();
}
return instance;
}
}
解释及优缺点:
代码中的getInstance方法加了synchronized 锁,是懒汉式线程安全的写法,但效率不高。
不加锁,是线程不安全方式的写法,严格意义上讲,不算单例模式。假如有两个线程A,B,同时调用getInstance方法,由于没有synchronized锁,有可能出现:线程A判断为null后被挂起,不new对象;然后执行线程B,发现也为空,new了对象;此时线程A被唤醒,接着执行new对象代码,现在就是两个对象了,违反单例模式定义。
优点:延迟加载(lazy load),真正用的时候才加载。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
不是延迟加载。
代码实现:
//饿汉式 单例模式
public class SingletonDemo1 {
//类初始化时,立即加载这个对象
private static SingletonDemo1 instance = new SingletonDemo1();
//构造器私有化。这样该类就不会被实例化
private SingletonDemo1(){
}
//获取唯一可用的对象
public static SingletonDemo1 getInstance(){
return instance;
}
}
解释及优缺点:
饿汉式单例模式在类SingletonDemo1加载时就初始化,并创建单例对象,绝对线程安全。
构造器私有化后,就不能通过new获取对象了。
优点:没有加锁,执行效率会提高。
缺点:由于类加载时就初始化,如果没有调用该类,会造成资源浪费。
JDK1.5 起
是延迟加载。
代码实现:
// 双检锁/双重校验锁 DCL,即 double-checked locking
public class SingletonDemo3 {
private volatile static SingletonDemo3 instance;
private SingletonDemo3(){};
public static SingletonDemo3 getInstance(){
if(instance == null){
synchronized (SingletonDemo3.class){
if (instance == null){
instance = new SingletonDemo3();
}
}
}
return instance;
}
}
解释及优缺点:
采用双锁机制,安全且在多线程情况下能保持高性能。
进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。
由于instance = new SingletonDemo3();
对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile
修饰instance实例变量有效,解决该问题。
实现比较困难,一般不推荐使用
是延迟加载。
代码实现:
public class SingletonDemo4 {
//静态类内部类
private static class SingletonHolder{
private static final SingletonDemo4 instance = new SingletonDemo4();
}
private SingletonDemo4(){}
public static SingletonDemo4 getInstance(){
return SingletonHolder.instance;
}
}
解释及优缺点:
SingletonDemo4类被装载了,instance不会被实例化SingletonHoldyanshijiazer类没有被主动使用,只有通过getInstance方法才会显式装载 SingletonHolder 类,从而实例化 instance。不像饿汉式那样立即加载。
静态内部类方式能达到双检锁方式一样的功效,但实现更简单。
如果想要实例化instance,但又想让它延迟加载,不想在类初始化的时候就被实例化,这种比较合适。
对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
JDK1.5 起
不是延迟加载。
代码实现:
public enum SingletonDemo5 {
//定义一个枚举的元素,他就代表了Singleton的一个实例。
INSTANCE;
//单例可以有自己的操作
public void singletonOperation(){
//功能处理
}
}
//测试类
class Test5{
public static void main(String[] args) {
SingletonDemo5 sd = SingletonDemo5.INSTANCE;
SingletonDemo5 sd2 = SingletonDemo5.INSTANCE;
System.out.println(sd==sd2);
}
}
解释及优缺点:
实现简单
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。
它更简洁,自动支持序列化机制,绝对防止多次实例化,避免反射和反序列化问题。
最后:
建议使用第 2种饿汉方式。如果有明确要求延迟加载的时候,建议使用第 5种静态内部类方式。若涉及到反序列化创建对象时,大家也可以尝试使用枚举方式。