设计模式-单例模式(Singleton)

简介

所谓类的单例设计模式,就是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例。怎么实现?具体有两种方式:饿汉式懒汉式,笔者是这么记忆的:饿汉式,比较饿,上来就需要造好对象。而懒汉式是啥时候需要就啥时候造。
具体实现如下:

饿汉式

public class Singleton {
   
    /* * 饿汉式 * */
    //1.私化类的构造器
    private Singleton() {
   
    }
    //2.内部创建类的对象
    //4.要求此对象也必须声明为静态的
    private static Singleton uniqueInstance = new Singleton();
    //3.提供公共的静态的方法,返回类的对象
    public static Singleton getInstance() {
   
        return uniqueInstance;
    }
}

懒汉式

public class Singleton {
   
    /* * 懒汉式 * */
    private Singleton() {
   
    }
    private static Singleton uniqueInstance = null;
    public static Singleton getInstance() {
   
        if (uniqueInstance == null) {
    // line A
            uniqueInstance = new Singleton(); // line B
        }
        return uniqueInstance;
    }
}

两种实现方式对比:
饿汉式:好处–是线程安全的,坏处–对象加载时间过长
懒汉式:好处–延迟对象的创建,坏处–线程不安全
写到这,可能不少读者就会想,懒汉式怎么就线程不安全了,其实我们可以设想有这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。刚刚判断完毕后,虚拟机将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然为空,因此线程二执行了new Singleton()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Singleton()操作,这样问题就来了,new出了两个instance,这还能叫单例吗?
显然这并不是单例模式,我们怎么改进多线程下的懒汉式让其变成单例模式呢?大部分人可能想到直接在方法上加入synchronized修饰符,这当然是可行的,具体实现如下:

public class Singleton {
   
    /* * 懒汉式 * */
    private Singleton() {
   
    }
    private static Singleton uniqueInstance = null;
    public synchronized static Singleton getInstance() {
   
        if (uniqueInstance == null) {
    // line A
            uniqueInstance = new Singleton(); // line B
        }
        return uniqueInstance;
    }
}

上面的实现方式是可以保证不会出现线程问题的,但细心的读者可能已经发现:<mark>除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上来说很不划算。</mark>,那可能会有读者说要不使用同步代码块吧,像下面这样的实现:

public class Singleton {
   
    /* * 懒汉式 * */
    private Singleton() {
   
    }
    private static Singleton uniqueInstance = null;
    public static Singleton getInstance() {
   
        synchronized (Singleton.class) {
   
            if (uniqueInstance == null) {
    // line A
                uniqueInstance = new Singleton(); // line B
            }
        }
        return uniqueInstance;
    }
}

但这样做对性能没有本质性的提升,回到我们最初的单例定义,我们所希望的是在第一次创建instance实例的时候进行同步,后续我们不再进行检查而是直接返回已创建好的实例对象。因此有了下面的写法——双重校验锁检查(DCL,Double Check Lock)

饿汉式(DCL)

public class Singleton {
   
    /* * 懒汉式 * */
    private static Singleton uniqueInstance = null;
    private Singleton() {
   
    }
    public static Singleton getInstance() {
   
        if (uniqueInstance == null) {
    // 线程二检测到uniqueInstance不为空
            synchronized (Singleton.class) {
   
                if (uniqueInstance == null) {
   
                    uniqueInstance = new Singleton(); // 线程一被指令重排,先只执行了赋值,但还没执行完构造函数(即未完成初始化)
                }
            }
        }
        return uniqueInstance; // 后面线程二执行时将引发:对象尚未初始化错误
    }
}

此时,我们看似已经满足线程安全的懒汉式单例了,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中。多么完美的实现呀!!!但JVM却给了我们狠狠一巴掌:如上代码段中的注释:假设线程一执行到uniqueInstance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1. 为 uniqueInstance 分配内存空间;
  2. 初始化 uniqueInstance;
  3. 将 uniqueInstance 指向分配的内存地址
    我们认为这些指令应该按着指定顺序进行执行,但问题就在这,<mark>JVM为了优化指令,提高程序运行效率,允许指令重排序</mark>(指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例)。所以,我们不得不去考虑一种特殊的执行顺序:
    a. 为 uniqueInstance 分配内存空间;
    b. 将 uniqueInstance 指向分配的内存地址;
    c. 初始化 uniqueInstance;
    这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
    <mark>具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)</mark>。
    综上所述,要解决这个问题,我们需要使用volatile关键字禁止指令重排序优化

饿汉式(最终版)

public class Singleton {
   
  /* * 懒汉式 * */
  private volatile static Singleton uniqueInstance = null;
  private Singleton() {
   
  }
  public static Singleton getInstance() {
   
      // 先判断对象是否已经实例过,没有实例过才进入加锁代码s
      if (uniqueInstance == null) {
   
          synchronized (Singleton.class) {
   
              // 类对象加锁
              if (uniqueInstance == null) {
   
                  uniqueInstance = new Singleton();
              }
          }
      }
      return uniqueInstance;
  }
}

单例设计模式应用场景

  1. <mark>网站的计数器</mark>,一般也是单例模式实现,否则难以同步。
  2. <mark>应用程序的日志应用</mark>,一般都使用单例模式是吸纳,这一般是共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  3. <mark>数据库连接池</mark>的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  4. 项目中,<mark>读取配置文件的类</mark>,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
  5. <mark>Application也是单例的典型应用</mark>
  6. Windows的<mark>TaskManager</mark>(任务管理器)就是很典型的单例模式
  7. Windows的<mark>Recycle Bin</mark> (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
全部评论

相关推荐

11-05 07:29
贵州大学 Java
点赞 评论 收藏
分享
1 收藏 评论
分享
牛客网
牛客企业服务