设计模式——0_4 单例(Singleton)

文章目录

  • 定义
  • 图纸
  • N个例子
    • 怎么保证别人不会自己去实例化单例类呢?
  • 写在最后的碎碎念
    • 延迟实例化 和 线程安全性
      • 双重检查加锁
    • 最后的最后

定义

保证一个类只有一个实例,并提供一个全局的访问点




图纸

设计模式——0_4 单例(Singleton)_第1张图片




N个例子

应用单例模式的例子实在是太多了,她几乎在程序中随处可见,比如代码里的工具包、应用程序里的注册表对象等等

她甚至还经常在其他的设计模式中有一定的戏份,比如工厂模式里的工厂类对象、生成器模式里的生成器对象、观察者模式里的主题对象……

基本上当你的对象符合以下条件的其中一种,你就可以考虑是否要使用单例模式了:

  1. 当这个类 只能 有一个对象,而且对这个对象的访问存在一个公开的访问点的时候
  2. 当这个类不存在状态,只负责输出方法的时候(比如工具类,他可以有多个实例,但是这些实例没有本质上的区别)



怎么保证别人不会自己去实例化单例类呢?

要不让用你写的代码的人在用之前都找你签个合同?


如果上面这个方法不好使的话,想想我们是怎么保护类里面的属性的呢?

我们会 私有化 类里面的属性,并提供 setter & getter 方法来统一对属性的访问


事实上要统一对单例对象的访问,用的也是这个思路,只不过我们要 私有化 的是类的构造器,就像这样:

public class A{
    
    private static A a = new A();
    
    public static A getInstance(){
        return a;
    }
    
    private A(){}
}

把构造方法设定成私有的,禁止别的类去访问,同时提供 getInstance 来统一对单例对象的访问




写在最后的碎碎念

延迟实例化 和 线程安全性

上例中的 A ,确实是一个单例实现,但是他并不优雅

他的问题主要出在以下两个方面:

  1. 静态块初始化的权利不在你手里,当有多个类牵扯其中的时候,有可能因为初始化顺序出现一些很微妙的bug,而且这些bug很难被找到原因
  2. 无论你有没有用到这个单例对象,因为单例对象是在静态块被实例化的,所以只要这个类被初始化了,这个单例对象就会被创建,即使你没有使用她

所以绝大多数情况下,我们会考虑把单例对象的实例化延迟,就像这样:

public class A{
    
    private static A a;
    
    public static A getInstance(){
        if(a == null){
            a = new A();
        }
        return a;
    }
    
    private A(){}
}

看起来很优雅吧,事实上除非你能保证A一定是在单线程情况下被访问,否则他比第一个实现更糟糕

因为他让A成了一个线程不安全的类

会出现这样的情况:

两个线程同时访问了getInstance方法,他们同时得到了a == null为true的结果;最终两个线程都创建了自己的单例对象,到这里这个单例实现就失控了

就像这样:

设计模式——0_4 单例(Singleton)_第2张图片


这个问题必须被解决,我们可以直接给 getInstance 方法上个锁,就像这样:

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

这样做可以解决线程问题,但出现了另一个问题

​ 因为上了锁,每一次获取A的单例对象的时候都必须要进行 获取&释放锁 的动作

可单例模式的价值就在于单例对象被频繁使用的时候可以免去重复初始化所带来的性能损耗,直接加锁的方式解决了问题,但是带来新的性能损耗,如果A只是一个可以被很简单就初始化的类,那么获取&释放锁的性能损耗可能比初始化带来的损耗还要大。属于是捡了西瓜丢了冬瓜了

当然,如果A并不会被频繁使用,而且加锁带来的性能损耗在可接受范围内,那么直接给 getInstance 加锁是最稳妥的方法

如果你追求极致,那可以考虑一下 双重检查加锁 的方案


双重检查加锁

即然给整个方法加锁会影响性能,那我只在需要初始化的时候加锁不就好了吗,就像这样:

public class A{
    
    private static volatile A a;//volatile保证在线程出现竞争的时候程序可以正常处理
    
    public static A getInstance(){
        if(a == null){
            synchronized(A.class){
				if(a == null){//双重检查,在锁内如果还是null,才需要初始化
		            a = new A();                                    
                }
            }

        }
        return a;
    }
    
    private A(){}
}

这样一来只有出现竞争的线程有机会获取锁,而在锁内也可以保持正常的行为;其他线程在第一次判断的时候就已经是false了,所以不会进锁,直接返还a



最后的最后

上文提到了单例的四种实现方式:静态初始化、不加锁、锁方法和双重检查加锁。事实上这四种方式不存在谁比谁高级,他们都是特定情况下的最优解。只不过代码性能越来越低(虽然是微乎其微的降低),安全性越来越高

这世上的事物都不会是非黑即白的,单例模式是这样,其他设计模式也一样。几乎所有的设计模式都有一个缺点,那就是让代码变得更复杂,可是她同时也让你的代码变得更清晰了或者性能提高了。凡事都有两面性,你必须在其中找到平衡点,找到当前形势下的最优解

你可能感兴趣的:(设计模式,单例模式,设计模式,java)