在 JavaEE 的编程实践中,单例模式是一种简洁而高效的设计模式,它确保了一个类只有一个实例,并提供了一个全局的访问点。今天,我们将一起探讨 JavaEE 中两种常见的单例模式实现方式:饿汉式和懒汉式。这两种方式各有千秋,不仅展示了编程的灵活性,也揭示了设计模式背后的深刻思考。接下来,我们将详细解析这两种模式的实现原理,并探讨懒汉式单例在应对线程安全挑战时的策略。让我们一同走进单例模式的世界,感受它的魅力吧。


一、啥是单例模式?

设计模式好比象棋中的“棋谱”。红方当头炮,黑方马来跳。针对红方的一些走法,黑方应招的时候有一些固定的套路。按照套路来走局势就不会吃亏。

软件开发中也有很多常见的”问题场景“。针对这些问题场景,大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏。

单例模式能保证某个类在程序中只存在唯一一份实例。而不会创建出多个实例(相当于这个类有且只有一个对象)。这一点在很多场景上都需要。比如 JDBC 中的 DataSource 实例就只需要一个。


二、单例模式的两种实现方式

而实现单例模式的具体方式又分为两种:饿汉模式 & 懒汉模式。

那什么是饿汉模式,什么又是懒汉模式呢?举个浅显易懂的例子,好比我在家里吃完饭之后洗碗,饿汉模式就是吃完立马洗,懒汉模式就是等下一顿再洗,需要几个盘子我洗几个。相比起饿汉模式,在不考虑卫生的情况下,懒汉模式更省时省力一些,具体代码实现,请往下看。

2.1、饿汉模式

// 饿汉模式
class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
public class Demo12 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

实现饿汉模式的方法很简单,我们的目标是让外部类不可以 new 我们设定类的对象,那我们直接用 private 把构造方法修饰起来就好了。

2.2、懒汉模式

懒汉模式和饿汉模式也差不多,也是用 private 修饰构造方法,但是我们要在返回对象的时候创建对象,这才能达到我们 “想要对象的时候再创建” 这条要求,具体代码实现如下:

1)第一版

// 懒汉模式
class SingletonLazy {
    private static SingletonLazy instance = null;

    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        // 如果为空,就赋值
        if (instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}
public class Demo13 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

这下我们饿汉模式和懒汉模式的代码可算是写完了,可是认真的观察之后,这两个模式的代码,在单线程还好,在多线程中,会不会出现问题呢???带着这个疑问,我们仔细观察发现饿汉模式还好,只涉及到读操作,而我们知道,读操作在多线程中是不会出现问题的,但是懒汉模式就不只是读了,在 getInstance() 这个方法中,是不是还涉及到写呢,写操作在多线程中就非常容易出 bug ,看下图分析:

JavaEE中的单例模式:饿汉与懒汉的优雅实现_volatile

这个都能看懂了吧?意思就是,在 s1 这个线程,刚判定完 instance 为空,准备赋值的时候, s2 线程这个时候插了一脚进来,那判定的不也为空了吗?这时候就完蛋了,直接返回了两个不同的 SingletonLazy 类的对象了,我们稍稍改变一下代码就可以观察出问题,如下:

// 懒汉模式
class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        // 如果为空,就赋值
        if (instance == null){
            // 加循环是为了把中间这段时间给扩大,让其他线程有更多的可乘之机
            for (int i = 0; i < 999999999; i++) {
                for (int j = 0; j < 999999999; j++) {
                }
            }
            instance = new SingletonLazy();
        }
        return instance;
    }
}
public class Demo13 {
    volatile public static SingletonLazy s1 = null;
    volatile public static SingletonLazy s2 = null;
    public static void main(String[] args) {
        while (true){
            Thread t1 = new Thread(() -> {
                s1 = SingletonLazy.getInstance();
            });
            Thread t2 = new Thread(() -> {
                s2 = SingletonLazy.getInstance();
            });

            t1.start();
            t2.start();
            if (s1 != s2){
                System.out.println(s1 == s2);
            }
        }
    }
}

这样子就很容易出现问题,运行结果如下:

JavaEE中的单例模式:饿汉与懒汉的优雅实现_饿汉模式_02

所以,这就有了我们的懒汉模式2.0版本。

2)第二版

那我们要如何解决这种问题呢?其实很简单,你既然在判断的时候容易出现线程不安全,那我直接加锁不久好了吗?其次,我们还要保证其他线程的内存可见性,还得在 instance 的前面加个 volatile ,所以我们的第二版代码如下:

// 懒汉模式
class SingletonLazy {
    volatile private static SingletonLazy instance = null;
    private SingletonLazy() {}
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        // 加锁保证赋值的时候一定是空的
        synchronized (locker){
            // 如果为空,就赋值
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}
public class Demo13 {
    volatile public static SingletonLazy s1 = null;
    volatile public static SingletonLazy s2 = null;
    public static void main(String[] args) {
        while (true){
            Thread t1 = new Thread(() -> {
                s1 = SingletonLazy.getInstance();
            });
            Thread t2 = new Thread(() -> {
                s2 = SingletonLazy.getInstance();
            });

            t1.start();
            t2.start();
            if (s1 != s2){
                System.out.println(s1 == s2);
            }
        }
    }
}

这个代码的线程安全问题算是没有了,可是在加锁的那个地方,大家仔细想想,这样写真的好吗?我们都知道,加锁对于 cpu 来说,是个比较大的开销,那我们第一次执行时加锁,没问题,那之后呢?是不是就不需要加锁了呀,这很简单,我们直接再在这之前判定一下是不是第一次就好了,所以第三版代码就现世了。

3)第三版

// 懒汉模式
class SingletonLazy {
    volatile private static SingletonLazy instance = null;
    private SingletonLazy() {}
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        // 判定是否是第一次
        if (instance != null) {
            return instance;
        }
        // 加锁保证赋值的时候一定是空的
        synchronized (locker){
            // 如果为空,就赋值
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}
public class Demo13 {
    volatile public static SingletonLazy s1 = null;
    volatile public static SingletonLazy s2 = null;
    public static void main(String[] args) {
        while (true){
            Thread t1 = new Thread(() -> {
                s1 = SingletonLazy.getInstance();
            });
            Thread t2 = new Thread(() -> {
                s2 = SingletonLazy.getInstance();
            });

            t1.start();
            t2.start();
            if (s1 != s2){
                System.out.println(s1 == s2);
            }
        }
    }
}

这就是我们最终版懒汉模式的代码了。


三、总结

经过对 JavaEE 中单例模式的深入探讨,我们详细对比了饿汉式和懒汉式两种实现方式,并重点关注了懒汉式实现中的线程安全问题。现在,让我们来总结一下这篇博客的核心内容。

首先,我们了解到单例模式在 JavaEE 开发中扮演着重要角色,它通过确保一个类只有一个实例并提供全局访问点,实现了资源的优化和系统效率的提升。在实际应用中,根据具体需求选择适当的单例模式实现方式至关重要。

接着,我们详细阐述了饿汉式和懒汉式两种实现方式的原理和特点。饿汉式在类加载时即完成实例的创建,实现简单且线程安全,但可能会造成资源浪费。而懒汉式则在第一次被使用时才进行实例化,实现了资源的延迟加载,但在多线程环境下需要特别注意线程安全问题。

针对懒汉式实现中的线程安全问题,详细讲解了使用 synchronizedvolatile 来保证线程安全,以及使用双重判定来提高运行效率。

最后,我们需要认识到单例模式并不是万能的,它也有其适用场景和限制。在使用单例模式时,我们需要仔细考虑其优缺点,并结合实际需求进行权衡和选择。同时,我们也需要关注单例模式的性能问题,特别是在高并发场景下,如何保证单例模式的性能和线程安全是我们需要重点关注的问题。

总之,通过对 JavaEE 中单例模式的深入探讨,我们不仅了解了其实现方式和原理,还掌握了解决线程安全问题的多种方法。希望这篇博客能够帮助大家更好地理解和应用单例模式,并在实际开发中取得更好的效果。


四、结语

探索JavaEE中的单例模式,不仅让我们在编程技术上获得了成长,更在人生道路上得到了深刻的启示。正如单例模式的独特之处,我们每个人都是独一无二的个体,在人生的旅途中,我们要坚持自己的信念,保持独特的个性,勇往直前。无论面临何种挑战和困难,都要相信自己,坚守初心,最终我们都能成为那个独一无二的、闪耀的“单例”。