JavaEE初阶 - 多线程基础篇 (饿汉模式与懒汉模式)

什么是单例模式?

单例模式的两种实现

饿汉模式

懒汉模式

两种模式的线程安全问题


1. 什么是单例模式?

单例模式是设计模式的一种, 设计模式就是针对这些场景给出一些经典的解决方案(类似于固定套路).

单例模式能保证某个类在程序中只存在一份实例, 不会创建出多个实例.

2. 单例模式的两种实现

  1. 饿汉模式:在类加载的同时创建实例.
  2. 懒汉模式:在类加载时不创建实例, 只有第一次使用这个类时才创建(也就是只有第一次创建实例时才会成功)

举一个生活中的例子, 吃完饭后, 饿汉模式会立刻将所有的碗洗掉(这里的"饿"有着急的意思), 而懒汉模式会先把脏碗放着, 等到下次吃饭的时候再洗, 在生活中, 我们更提倡第一种做法, 但是在计算机中, 懒汉模式更为常用.

3. 饿汉模式

//创建饿汉模式
class Singleton{
    
    //使用static创建实例, 并立即对其实例化
    // instance即为饿汉模式创建的唯一实例
    private static Singleton instance = new Singleton();

    //将Singleton类的构造器设为私有, 防止在类的外部产生其他的Singleton实例
    private Singleton(){}

    //提供get方法, 使外部仅能通过get方法获取到这个实例
    public static Singleton getInstance(){
        return instance;
    }
}
//从外部获取Singleton的唯一实例
public class Demo {
    public static void main(String[] args) {
        //外部仅能通过这种方法获取到Singleton的实例, 且无法创建其他的实例
        Singleton instance = Singleton.getInstance();
    }
}

  使用饿汉模式, 类的外部就仅能通过getInstance()方法获取到Singleton的这一个实例, 而无法通过new的方式产生其他实例.

4. 懒汉模式

//创建懒汉模式
class Singleton{
    //类加载时, 先不进行实例化对象
    private static Singleton instance = null;

    //将构造器设为私有, 防止在类的外部产生其他的Singleton实例
    private Singleton(){}

    //在第一次获取Singleton对象时, instance为空, 此时进行实例化对象
    //之后再试图获取时, instance已不为空, 无法再进行实例化
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

//获取Singleton对象
public class Demo {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
    }
}

  懒汉模式不会在类加载的过程中直接实例化对象, 而是当用户第一次调用getInstance()方法时再实例化对象, 之后用户再调用getInstance()方法时, 只会直接返回原有的对象, 而不会再次产生新的对象.

5. 两种模式的线程安全问题

  上面的两种模式中, 饿汉模式只涉及到了读操作, 并没有涉及对共享数据的修改, 因此饿汉模式是线程安全的, 而懒汉模式即包含了读操作, 又包含了写操作, 并且读和写操作并不具有原子性, 因此懒汉模式是线程不安全的.

要想保证懒汉模式的线程安全, 我们就需要对其进行加锁操作:

public static Singleton getInstance() {
    synchronized (Singleton.class) {
        if (instance == null) {
            instance = new Singleton();
        }
    }
    return instance;
}

  对上面的懒汉模式进行加锁, 确实可以解决线程安全问题, 但是, 懒汉模式的写操作只发生在instance实例化之前(在instance实例化之后, instance一定不为空, 不会进入if 语句, 也就不涉及写操作), 也就是说, 只要instance被实例化完成, 懒汉模式就不涉及写操作, 也就不涉及线程安全问题了, 但我们的加锁操作是针对全局进行加锁的, 这样多余的操作会产生大量无用的锁竞争操作, 大大减缓了运行效率.

对此, 我们需要在锁的外层再进行一次判定, 使得代码只在instance被实例化之前进行加锁:

//在锁的外面加上一层判断条件
if(instance == null) {
    synchronized (Singleton.class) {
        if (instance == null) {
            instance = new Singleton();
        }
    }
}

注意:这里的两层判断条件看起来是相同的, 但它们执行的效果和时机是完全不同的, 外层判定的是是否要进行加锁, 内层判定的是是否能够创建实例, 并且加锁可能会导致线程产生阻塞, 在两次判定的中间, instance可能会被其他线程所修改.

对于这个问题, 我们只需要对instance加上volatile关键字即可.

private static volatile Singleton instance = null;

总结:要让线程不安全的懒汉模型变为线程安全, 我们需要作出如下调整:

  1. 在正确的位置进行加锁
  2. 双重if判断条件(注意两次判定的不同效果和时机)
  3. 对instance添加volatile关键字

完整的线程安全的懒汉模式代码如下:

class Singleton {
    //类加载时, 先不进行实例化对象, 这里记得为instance添加volatile关键字
    private static volatile Singleton instance = null;

    //将构造器设为私有, 防止在类的外部产生其他的Singleton实例
    private Singleton() {
    }

    //在第一次获取Singleton对象时, instance为空, 此时进行实例化对象
    //之后再试图获取时, instance已不为空, 无法再进行实例化
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
//获取Singleton对象
public class Demo2 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
    }
}

The end

你可能感兴趣的:(JavaEE初阶,学习,java-ee,java)