【Java多线程案例】单例模式

1. 单例模式概念

设计模式:谈到单例模式,我们首先需要知道什么是设计模式,设计模式是软件工程中的一大重要概念,是被广泛认可并使用于解决特定实际问题的代码设计经验,校招中常考的设计模式有单例模式工厂模式 等,而我们需要重点掌握单例模式代码的编写

简单来说,设计模式就是大佬们为了不让我们这些小菜鸟写烂代码而总结出来的代码编写方式

单例模式:单例模式要求类在一个Java进程只能拥有唯一一个实例,而无法创建出多个实例(尝试使用new关键字创建多个实例的时候就会报错)

2. 单例模式代码示例

简单介绍了设计模式以及单例模式的相关概念,光说不练假把式,现在我们就来尝试编写单例模式的代码,在正式编写代码之前,我们需要知道单例模式有两种实现方式:1、饿汉模式;2、懒汉模式,二者之间的差异就在于创建唯一实例的时机不同!

2.1 饿汉模式

2.1.1 代码案例

/**
 * 单例模式类
 */
class Singleton {
    private static Singleton instance = new Singleton(); // 指向唯一实例

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {};

}

public class SingletonDemo01 {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2);
    }
}

运行结果如图所示:
【Java多线程案例】单例模式_第1张图片

2.1.2 代码分析

  1. 首先观察唯一实例的创建时机private static Singleton instance = new Singleton();该实例在类加载的时候就创建完毕,因此称为"饿汉式"
  2. 再来观察单例模式中的构造方法,普通类的构造方法一般是public修饰的,但是单例模式中将构造方法私有化使用private修饰,因此外界尝试使用new关键字创建实例就会报错
  3. 单例模式提供了一个public static修饰的获取唯一实例的方法,外界只能通过调用这个方法来获取该类的唯一实例,这样就实现了该类只能拥有唯一实例的要求

我相信有的小伙伴一定此时忍不住发出疑问!如果使用反射机制那不就可以创建出多个实例了嘛?事实上如此,但是毕竟反射不属于常规方法,毕竟在代码中滥用反射是非常不好的行为!!!

2.2 懒汉模式

2.2.1 代码案例

/**
 * 懒汉式单例模式
 */
class SingletonLazy {
    private static SingletonLazy instance = null; // 唯一实例

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy() {};

}

2.2.2 代码分析

  1. 懒汉模式的唯一实例并不是在类加载的时候就创建完毕,而是遵循"能不创建就不创建,能晚创建就晚创建"的原则,直到外界有程序调用getInstance方法获取实例时才创建完毕,这就是"懒汉式"的由来
  2. 懒汉模式与饿汉模式一致,都将构造方法设置成私有
  3. 懒汉模式与饿汉模式一致,都提供getInstance方法供外界获取该类的唯一实例

注:在计算机世界中"懒"往往意味着效率高,考虑这样一个场景,有一个10GB的文件,你使用文本编辑器打开,如果采用饿汉式的方式,系统会将10GB的文件一次性加载到内存中,然后统一展示;然而如果使用懒汉式方式打开,则编辑器先加载10KB文件让用户阅读,随着用户进行翻页操作再继续加载10KB文件到内存中。此时懒汉式无疑效率更高

3. 单例模式的线程安全问题

3.1 问题引入

上述关于"单例模式"的介绍只是序幕,毕竟我们本章重点论述的还是多线程主题,下面就有一个重要问题了:上述我们编写的单例模式代码是否存在 线程安全问题 呢?

  • 饿汉模式:由于饿汉模式中创建唯一实例的时机在类加载的时候,而调用getInstance方法执行的过程中直接返回该唯一实例,是纯粹的读取操作,不涉及多个线程修改同一变量的情况,因此天然就是线程安全的!
  • 懒汉模式:想必聪明的小伙伴已经想到了,懒汉模式是不是就是线程不安全的呢?就是这样!下面我们就来考虑多个线程同时执行的情况

演示懒汉模式的线程安全问题
【Java多线程案例】单例模式_第2张图片
如图所示:线程t1首先进入条件判断当前实例为null,然后进入if语句块中,但是此时还没有来得及执行new操作创建实例就被调度出CPU,此时线程t2进行判断当前实例为null,然后进入if语句块中创建实例完成返回,此时线程t1重新被CPU调度,接着执行new SingletonLazy()方法,因此创建出了多个实例,即懒汉式单例模式线程不安全!!!

3.2 解决懒汉式线程安全问题

那么如何改进上述代码让"懒汉式"单例模式变成线程安全的呢?出现问题的关键在于if条件判断和new创建实例并不是原子操作,因此解决方法就是通过synchronized关键字将这两个操作打包成原子操作!

3.2.1 改进代码(解决线程安全问题)

/**
 * 改进版本单例模式(解决线程安全问题)
 */
class SingletonLazyImprove01 {
    private static SingletonLazyImprove01 instance = null; // 唯一实例
    private static Object locker = new Object();

    public static SingletonLazyImprove01 getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazyImprove01();
            }
        }
        return instance;
    }

    private SingletonLazyImprove01() {};
}

上述代码我们引入synchronized关键字将if条件判断与new创建实例操作加锁打包成"原子"操作,此时如果继续按照刚才的场景,线程t1先进行加锁,如果t2也尝试进入if语句块就会因为锁竞争而阻塞等待,直到线程t1创建完实例之后释放锁,此时t2线程判断实例已经被创建好,就直接返回,因此不会出现线程安全问题了!

3.2.2 改进代码(解决效率问题)

但是问题还没有结束!现在的代码虽然解决了线程安全问题,但是还存在着效率问题,因为我们这里调用getInstance都会先尝试加锁,然后判断当前实例是否为空,然后再解锁!但是线程安全问题只存在于第一次尝试创建实例的时候,如果实例已经创建完毕,后续所有的操作都是读取操作,就不会再产生线程安全问题了!所以我们引入的解决方式就是 if双重校验锁 ,其代码如下:

class SingletonLazyImprove02 {
    private static SingletonLazyImprove02 instance = null;
    private static Object locker = new Object();

    public static SingletonLazyImprove02 getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazyImprove02();
                }
            }
        }
        return instance;
    }

    private SingletonLazyImprove02() {};
}

此时我们会看到有两个if判断语句,换做是单线程情况下我们从来不会写这样的代码,但是一旦涉及到多线程的时候,if双重校验锁是很常见的,其中第一个if语句用来判断是否需要进行加锁操作,第二个if语句用来判断是否需要创建实例,但是恰巧两个if语句的条件一样!

3.2.3 改进代码(解决指令重排序问题)

但是上述代码还有一些小问题,我们需要先介绍有关 指令重排序 的话题

指令重排序:指令重排序是一种编译器优化手段,会在保证逻辑不变的情况下调整原有代码的执行顺序,来提高程序的效率,但是有可能会引发线程安全问题
例如在创建实例的代码中:instance = new SingletonLazyImprove02();内部包括三个核心步骤

  1. 申请一块内存空间
  2. 调用构造方法对内存空间进行初始化
  3. 将内存空间地址赋值给instance变量

正常情况下执行顺序都是按照1->2->3进行的,但是编译器也可能会优化为1->3->2的步骤来执行,这两种在单线程环境下都没有问题,但是如果在多线程情况下执行就有可能出现线程安全问题
【Java多线程案例】单例模式_第3张图片
如图所示,线程t1先进行加锁,然后创建实例过程中先执行第一步与第三步,此时被调度出CPU,但是后来的线程判断实例是否为空,此时直接返回未被初始化的内存空间地址,这时就会出现错误!!!

volatile关键字:我们在之前的章节已经提到过使用volatile关键字可以解决内存可见性和指令重排序的问题,因此该问题的解决方式就是使用volatile关键字,强制让编译器完全按照1->2->3的顺序来执行

/**
 * 懒汉式单例模式改进版本(解决指令重排序)
 */
class SingletonLazyImprove03 {
    private static volatile SingletonLazyImprove03 instance = null;
    private static Object locker = new Object();

    public static SingletonLazyImprove03 getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazyImprove03();
                }
            }
        }
        return instance;
    }

    private SingletonLazyImprove03() {};
}

你可能感兴趣的:(多线程编程,java,单例模式)