在 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 ,看下图分析:
这个都能看懂了吧?意思就是,在 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);
}
}
}
}
这样子就很容易出现问题,运行结果如下:
所以,这就有了我们的懒汉模式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 开发中扮演着重要角色,它通过确保一个类只有一个实例并提供全局访问点,实现了资源的优化和系统效率的提升。在实际应用中,根据具体需求选择适当的单例模式实现方式至关重要。
接着,我们详细阐述了饿汉式和懒汉式两种实现方式的原理和特点。饿汉式在类加载时即完成实例的创建,实现简单且线程安全,但可能会造成资源浪费。而懒汉式则在第一次被使用时才进行实例化,实现了资源的延迟加载,但在多线程环境下需要特别注意线程安全问题。
针对懒汉式实现中的线程安全问题,详细讲解了使用 synchronized
和 volatile
来保证线程安全,以及使用双重判定来提高运行效率。
最后,我们需要认识到单例模式并不是万能的,它也有其适用场景和限制。在使用单例模式时,我们需要仔细考虑其优缺点,并结合实际需求进行权衡和选择。同时,我们也需要关注单例模式的性能问题,特别是在高并发场景下,如何保证单例模式的性能和线程安全是我们需要重点关注的问题。
总之,通过对 JavaEE 中单例模式的深入探讨,我们不仅了解了其实现方式和原理,还掌握了解决线程安全问题的多种方法。希望这篇博客能够帮助大家更好地理解和应用单例模式,并在实际开发中取得更好的效果。
四、结语
探索JavaEE中的单例模式,不仅让我们在编程技术上获得了成长,更在人生道路上得到了深刻的启示。正如单例模式的独特之处,我们每个人都是独一无二的个体,在人生的旅途中,我们要坚持自己的信念,保持独特的个性,勇往直前。无论面临何种挑战和困难,都要相信自己,坚守初心,最终我们都能成为那个独一无二的、闪耀的“单例”。