vlambda博客
学习文章列表

专栏:设计模式-单例模式


图片 | Google

文章 | 靳天
校对  靳天

概念

  java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例。

  

  单例模式有以下特点:

  

  1、单例类只能有一个实例。

  

  2、单例类必须自己创建自己的唯一实例。

  

  3、单例类必须给所有其他对象提供这一实例。

  

  

  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

  

懒汉式非线程安全单例

/** * @author sjt * @Date 2020-04-07 00:59:04 *///懒汉非线程安全-第一次调用时实例化public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
    public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); }        return singleton; }}

  

懒汉式线程安全单例

不了解并发编程的请移步饿汉单例,跳过此节,最后再看。


1. synchronized同步

 /** * @author sjt * @Date 2020-04-07 00:59:04   */ public class SecuritySingleton {    private SecuritySingleton(){}    private  static SecuritySingleton singleton = null;        public synchronized static SecuritySingleton getInstance(){       if(singleton == null){         singleton = new SecuritySingleton();     }      return singleton;    }  }

解析

仅仅用synchronized修饰getinstance方法,就可以达到线程安全。


原因:synchronized修饰静态方法,作用范围是修饰的方法。

当多线程且执行到getinstance方法时,synchronized会锁住当前类,同一时刻只有一个线程可以执行getinstance方法,其他线程均阻塞。


更通俗的说,就是多线程操作时,每个线程都会执行代码块,也就是代码块会同时执行,那么不加synchronized线程同步的情况下,

可能new SecuritySingleton();会同时执行多次,得到返回不同的实例,造成并发错误。


也就是说,在多线程情况下,synchronized修饰的代码块会串行执行。



这样虽然保证了并发安全,但是同一时间只能执行一个线程,大大降低了效率。更可以说是这个锁的粒度太大了,并发效率和并发安全的权衡可以通过锁的粒度控制。


2. double-checked lock

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

解析

二次检测锁可以达到线程安全。

注意!singleton 静态类成员变量多加了volatile修饰,具体详见[2]。


以下都是极大多数情况,不包括极端例子。

简单讲就是多个线程同时执行到line 5,此时每个线程都认为singleton==null

synchronized仍然使锁住的代码块串行执行,此时cpu调度某一个线程执行,其他线程阻塞。这个被调度的线程仍然认为Singleton==null,然后初始化得到一个实例。


假设new SecuritySingleton()正常执行完毕(绝大数情况),这样singleton赋值成功一个实例,对所有其他的线程均可见。也就表示所有线程的singleton都是同一个实例,成功单例化。


但是极少数情况new SecuritySingleton()会执行出错,得到一个不完整的实例。这样其他线程可能同步了一个不完整的实例,这就需要volatile来修正。


为什么new SecuritySingleton()会执行出错?

为什么volatile可以修正这个问题?


留一个小问题,希望你在后台留言给我答案。


3. 静态内部类

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

3比1,2来说最好。

3不用同步减低效率,也不像2一样难理解和某些隐藏问题。

内部类初始化时就实例化singleton,天生线程安全。

加final修饰方法是若有继承关系,保证子类不能重写父类方法。


饿汉式线程安全单例

/** * @author sjt * @Date 2020-04-07 00:59:04 *///饿汉:类初始化就实例化,天生线程安全public class Singleton { private Singleton(){}    private static final Singleton  singleton = new Singleton(); public static Singleton getInstance(){ return singleton;    }}


饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,


饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,


而懒汉比较懒,只有当调用getInstance的时候,才会去初始化这个单例。


另外从以下两点再区分以下这两种方式:


1、线程安全:


饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,


懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。


2、资源加载和性能:


饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,


而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。


至于1、2、3这三种实现又有些区别,


第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,


第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗


第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。


文末小问题

为什么在懒汉double-checked locking 方法中new SecuritySingleton()会执行出错?是怎样的一种并发错误?


为什么volatile可以修正这个问题?


可以参考文末[2]。


留一个小问题,希望你在后台留言给我答案。


参考资料

[1]. [23种设计模式汇总整理]

(https://blog.csdn.net/jason0539/article/details/44956775) 


[2]. [double-check method潜在问题解决-volatile修饰解决]

(http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)



还可以看

[设计模式汇总贴]

(https://shaojintian.cn/2020/04/07/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%B1%87%E6%80%BB%E8%B4%B4/)



如果本文有什么笔误、错误,请您在订阅号后台直接留言,作者看到后会及时勘误的!(蟹蟹)

点在看好不好,喵~