1、定义

指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。例如任务管理器任何时候都只能打开一个。就是运用了单例模式。

1.1 特点

  1. 单例类只有一个实例对象
  2. 单例类只能自己创建自己的唯一实例对象。
  3. 单例类必须给其他所有对象提供这一实例。

    1.2 结构

    3.png

    1.3 实现的关键点

  4. 将类的构造方法设置为私有方法。这样其他类就不能通过调用该类的构造方法来实例化对象。

  5. 定义一个私有的类的静态实例。
  6. 提供一个公有的获得静态实例的方法。


2、单例模式的实现

2.1 饿汉式单例

饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。就跟名字一样,像一个饿汉一样,饥不择食,在类装载的时候就创建,不管用不用,先创建了再说。

优缺点:
优点:**如果一直没有被使用,便浪费了空间,典型的空间换时间,如果初始化很耗费时间,同时也会 推迟系统的启动时间。
缺点:每次调用的时候,就不需要再判断,节省了运行时间。**

  1. public class Singleton {
  2. //定义一个私有静态实例instance
  3. private static Singleton instance = new Singleton();
  4. //定义私有的构造方法
  5. private Singleton(){}
  6. //只能通过该方法获取唯一的instance实例
  7. public static Singleton getInstance(){
  8. return instance;
  9. }
  10. }

2.2 懒汉式单例

非线程安全:

  1. public class Singleton {
  2. //先定义一个私有静态变量,不生成实例
  3. private static Singleton instance;
  4. //定义私有的构造方法
  5. private Singleton(){}
  6. //只能通过该方法获取唯一的instance实例
  7. public static Singleton getInstance(){
  8. //懒汉式单例在用户第一次调用时再进行初始化
  9. if (instance == null){
  10. instance = new Singleton();
  11. }
  12. return instance;
  13. }
  14. }

从代码中可以看出,懒汉式单例只有在第一次调用才会初始化。

优缺点:
优点:使用时才创建,节省了资源。
缺点****第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作,很可能会造成多次实 例化,就不再是单例了。

线程安全:**

  1. public class Singleton {
  2. //先定义一个私有的静态变量
  3. private static Singleton instance;
  4. //定义私有的构造方法
  5. private Singleton(){}
  6. //为该方法加上synchronized关键字,可以实现线程安全
  7. public static synchronized Singleton getInstance(){
  8. if (instance == null){
  9. instance = new Singleton();
  10. }
  11. return instance;
  12. }
  13. }

优缺点:
优点:**可以在多线程环境下确保只初始化一个单例。
缺点其实只有第一次调用getInstance时才真正需要同步,一旦设置好instance实例后,之后每次同步都 是累赘,平白增添性能消耗

2.3 双重校验锁

对懒汉式单例的改进。


如果程序可以接受synchronized带来的性能负担,“懒汉式(线程安全)”可以使用,但如果很关心性能,“双重校验锁(DCL)”将会带来很大帮助。(double checked locking)

  1. public class Singleton {
  2. //定义一个私有的静态变量,注意这里的volatile关键字,这是必须的
  3. private static volatile Singleton instance;
  4. //定义私有的构造方法
  5. private Singleton(){}
  6. public static Singleton getInstance(){
  7. //第一个校验是是为了提高代码效率
  8. if (instance == null){
  9. synchronized (Singleton.class){
  10. if (instance == null){
  11. instance = new Singleton();
  12. }
  13. }
  14. }
  15. return instance;
  16. }
  17. }

在getInstance()方法中对instance进行了两次判空。

  • 第一次是为了提高代码效率,由于单例模式只需要创建一次实例即可。所以当创建了一个实例之后,再次调用该方法就不需要进入同步代码块去竞争锁了。直接返回前面创建的实例即可。
  • 第二次是为了防止二次创建实例。

举一个例子
当单例还没有被创建时,线程t1调用getInstance( )方法。第一次判断Singleton==null时,t1准备继续执行,而这时资源被线程t2抢占了。t2同样调用该方法,这时单例还没有被创建,顺利通过第一个判断if。遇到第二个判断if后,单例没有被创建,顺利通过,创建实例,任务完成。资源这时重新回到线程t1,这时遇到第二个判断,由于实例已创建,不能通过。避免了多线程创建多个实例的错误。


volatile关键字的作用
在创建私有静态变量时,注意此处使用的关键字volatile。它的作用是可以防止JVM指令重排优化

instance = new Singleton() 执行时,JVM指令的执行顺序如下:

  1. 为变量instance分配内存空间。
  2. 初始化instance。
  3. 将变量instance指向分配的内存空间。

但是JVM具有指令重排的特性,有可能执行顺序会变成1-3-2。指令重排在单线程的情况下不会出现问题,但是在多线程的情况下,会导致一个线程获得一个未初始化的实例。使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。

举一个例子
线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。

volatile还有第二个作用
保证变量在多线程运行时的可见性。
在JDK1.2以前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就 可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取

2.4 静态内部类

  1. public class Singleton{
  2. //用一个静态内部类来初始化实例
  3. private static class Holder{
  4. private static final Singleton instance = new Singleton();
  5. }
  6. //定义私有的构造方法
  7. private Singleton(){};
  8. //提供一个获取实例的方法
  9. public static final Singleton getInstance(){
  10. return Holder.instance;
  11. }
  12. }


3、单例模式的应用场景

  1. windows系统的任务管理器,你永远只能打开一个任务管理器。
  2. windows系统的回收站。