单例模式绝对没有你想象的那么简单!不服来战!

Posted 程序编织梦想

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式绝对没有你想象的那么简单!不服来战!相关的知识,希望对你有一定的参考价值。

一、前言

单例模式(Singleton Pattern)是 Java 中最常用的设计模式之一,同时也是面试的重灾区。有些人可能觉的单例模式很简单,没有什么难的。其实不然,因为牵扯到线程安全的问题,所以单例模式绝对能体现出你的功底。不信接着往下看。

二、单例模式详解

单例模式大体分为二种写法:饿汉式和懒汉式。

1.饿汉式

这种方式最简单,所以我们先把这种方式介绍一下,代码如下:

public class Singleton   
    private static Singleton instance = new Singleton();  
    private Singleton ()  
    public static Singleton getInstance()   
    return instance;  
      

这种方式优点就是效率高、写法简单并且是线程安全的。但是缺点就是在类加载的时候就要初始化,浪费内存。
饿汉式单例天生就是线程安全的。饿汉式在类加载过程中就会初始化,因为类在加载过程中会加锁,所以线程安全。

2.懒汉式

懒汉式的特点是在第一次调用的时候才初始化,避免浪费内存。懒汉式是面试重灾区,为了让大家了解每一种写法存在的问题,我们从简单到复杂一步步写。

2.1 懒汉式初级写法(线程不安全):

public class Singleton   
    private static Singleton instance;  
    private Singleton ()  
  
    public static Singleton getInstance()   
    if (instance == null)   // 问题出在这里,导致线程不安全。
        instance = new Singleton();  
      
    return instance;  
      

这个写法之所以不安全,是因为当有多个线程同时进入 if (singleton2 == null) … 语句块的时候,该单例类有可能会创建出多个实例,违背单例模式的初衷,因此,传统的懒汉式单例是非线程安全的。

2.2 懒汉中级写法(线程安全)

既然上面的写法线程不安全,那么我们在getInstance()方法上加一把锁,代码如下:

// 线程安全的懒汉式单例
public class Singleton 
    private static Singleton singleton;
    private Singleton()
    // 使用 synchronized 修饰,临界资源的同步互斥访问
    public static synchronized Singleton getSingleton()
        if (singleton2 == null) 
            singleton2 = new Singleton2();
        
        return singleton2;
    

该实现与上面2.1传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。

这种写法乍一看没问题,但是这种实现方式的运行效率会很低,因为我们把整个getSingleton()加锁,同步块的作用域有点大,而且锁的粒度有点粗,所以我们继续升级写法。

2.3 双重校验锁写法(线程不安全)

public class Singleton   
    private  static Singleton singleton;  
    private Singleton ()  
    public static Singleton getSingleton()   
    if (singleton == null)   
        synchronized (Singleton.class)   
        if (singleton == null)   
            singleton = new Singleton();  //位置1:问题出在这里
          
          
      
    return singleton;  
      

这种写法也是有问题的。为什么呢?问题出在singleton = new Singleton();这条语句上。

singleton = new Singleton();这条语句在创建对象的过程中会分成3个步骤,如下:

memory = allocate();  // 步骤1.分配对象的内存空间
ctorInstance(memory);  // 步骤2.初始化对象
sInstance = memory;  // 步骤3.设置sInstance指向刚分配的内存地址

JVM在执行的过程中步骤2和步骤3可能会发生指令重排序,重排后执行顺序如下:

memory = allocate();  // 步骤1.分配对象的内存空间
sInstance = memory;  // 步骤2.设置sInstance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory);  // 步骤3.初始化对象

这种指令重排序在单线程下不会有问题,但是在并发情况下就会出现问题,如下图:

我觉的这张图我画的很明白了,大家应该是可以看懂的。因为并发的原因线程2访问到的是一个还未初始化的对象。这种不安全的因素是极难复现的,但是理论上还是存在线程不安全的因素。所以我们还要继续改进。

2.4 volatile修饰写法(线程安全)

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

这个方式和上面双重锁检查写法唯一的区别就是加volatile来修饰singleton。
volatile要仔细解释起来篇幅就大了,本章主要介绍单例,所以由于篇幅的问题,这里只是简单介绍一下volatile关键字。volatile有三大作用:

1.可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2.原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行、
3.有序性:被volatile修饰的变量不会发生指令重排序。

2.5 静态内部类写法(线程安全)

public class Singleton   
    private static class SingletonHolder   
    		private static final Singleton INSTANCE = new Singleton();  
      
    private Singleton ()  
    public static final Singleton getInstance()   
    return SingletonHolder.INSTANCE;  
      

我们上面提到过,类的加载机制是线程安全的。这种方式能达到双检锁方式一样的功效,但实现更简单,对静态域使用延迟初始化。

结尾

好了,本章就讲到这里吧。怎么样,单例模式是不是没你想象的那么简单。
有什么问题留言,也可以去我的微信公众号留言。
大家帮忙微信关注我的微信公众号,每天提供优质java知识,并领取很多视频学习资料。

扫二维码关注公众号【Java程序员的奋斗路】可领取如下:
1.学习资料: 1T视频教程(大约有100多个视频):涵盖Javaweb前后端教学视频、机器学习/人工智能教学视频、Linux系统教程视频、雅思考试视频教程,android.等
2.项目源码:20个JavaWeb项目源码。

以上是关于单例模式绝对没有你想象的那么简单!不服来战!的主要内容,如果未能解决你的问题,请参考以下文章

csp模拟赛1不服来战 (challenge.cpp)

最难的10个Java面试题,不服来战!

源码大招:不服来战!撸这些完整项目,你不牛逼都难!

世上最全计算机网络面试整理(附答案),不服来战!!

第二弹:史上最全操作系统面试整理(附答案),不服来战!!

「仅限100人」10天LeetCode100题,不服来战!