单例模式:懒汉和饿汉
目录
一、关于设计模式
二、单例模式是什么
2.1 饿汉模式
2.2 懒汉模式
三、单例模式和多线程
3.1 饿汉模式
3.2 懒汉模式
一、关于设计模式
单例模式是一种设计模式,说它之前先来聊聊设计模式是什么。
设计模式,类似于于棋谱(大佬把一些对局整个推演过程写出来)
设计模式,就相当于程序员的棋谱。大佬们把一些典型的问题场景,整理出来,并且针对这些场景,代码该怎么写,具体方案给出一些指导和建议。
框架是属于‘硬性要求’,设计模式是‘软性要求’,目标是一致的。
二、单例模式是什么
单例模式是设计模式中经典也是比较简单的模式
单个实例(对象)
强制要求,某个类,在某个程序中,只有唯一一个实例(不允许创建多个实例,不允许new多次)
class Test{}//对象/实例
Test t = new Test();
单例模式,强制要求一个类不能创建多个对象,通过一些编程技巧,达成上述的强制要求。
在代码中,如果创建多个实例,直接编译失败
单例模式两种情况:饿汉模式和懒汉模式。接下来会根据这两种情况进行展开
2.1 饿汉模式
饿,代表着迫切,想要尽早创建实例
class Singleton{//静态成员的初始化,是在类加载的阶段出发的//类加载往往就是在程序已启动就会触发private static Singleton instance = new Singleton();//后续通过get方法获取这里的实例public static Singleton getInstance(){return instance;}//单例模式中的“点睛之笔”,在外面进行new操作,都会编译失败private Singleton() {}
}public class demo1 {public static void main(String[] args) {Singleton t1 = Singleton.getInstance();Singleton t2 = Singleton.getInstance();System.out.println(t1 == t2);//会报错//Singleton t3 = new Singleton();}
}
2.2 懒汉模式
懒 和 饿 是相对的,懒是尽量晚的创建实例(甚至可能不创建了),延迟创建
懒 在计算机里是褒义词,另一个含义,是高效率
//懒汉模式
class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}public class demo2 {public static void main(String[] args) {SingletonLazy t1 = SingletonLazy.getInstance();SingletonLazy t2 = SingletonLazy.getInstance();System.out.println(t1 == t2);//SingletonLazy t3 = new SingletonLazy();}
}
三、单例模式和多线程
上述内容,都是引子,接下来才是正题
上述懒汉/饿汉模式,是否是线程安全?如果不是,该咋办?
这两个版本的getInstance在多线程环境下调用,是否会出bug?
我们可以一个个来看
3.1 饿汉模式
class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;}private Singleton() {}
}
这里只涉及了 return,而return是 读操作,线程安全
String 不可变对象,天然线程安全
3.2 懒汉模式
class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}
if(instance == null){
instance = new SingletonLazy();
}
这部分可能会涉及到 多线程 的修改
= 操作是原子的, += -= 这些是非原子的
这里可能会出现bug,这就导致懒汉模式这个写法,getInstance方法是线程不安全的。
怎么解决?
加锁是一个常规的操作
但也要注意加锁的位置,我们希望的是:
条件和修改都能打包成原子的操作
private static Object locker = new Object(); public static SingletonLazy getInstance() {synchronized(locker){if(instance == null){instance = new SingletonLazy();}}return instance;}
不是写了synchronized,代码就一定安全,一定得具体问题具体分析
引入加锁后,后执行的线程就会在加锁位置阻塞,阻塞到前一个线程解锁,当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会进行后续new的操作。
也可以进行方法加锁
public synchronized static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}
但是
加锁引入新的问题:
当把实例创建好之后,后续再调用getInstance,此时都是直接执行return,如果只是进行if判定+return,纯粹的读操作了,读操作不涉及线程安全问题
但是,每次调用上述方法,都会触发一次加锁操作,虽然不涉及线程安全问题,但是多线程情况下,这里的加锁,就会相互阻塞,影响程序的执行效率
所以我们可以这样:按需加锁
真正涉及到线程安全的时候,再加锁,不涉及的时候,就不加锁
如果实例已经创建过了,就不涉及线程安全问题提。如果还没创建,就涉及线程安全问题
public static SingletonLazy getInstance() {if(instance == null){ // 判断是否需要加锁synchronized (locker){ // if(instance == null){ // 判断是否需要new对象instance = new SingletonLazy();}}}return instance;
}
单线程中,连续两个相同的if,是毫无意义的,单线程中,执行流就只有一个,上一个if和下一个if是一样的
但是多线程中,两次判定之间,可能存在其他线程,就把if中的Instance变量给修改了,也就是导致了这里的两次if的结论可能不同
再仔细分析,上述代码,仍然存在问题:
t1线程在读取Instance的时候,t2线程进行修改,是否存在内存可见性问题?
可能存在,编译器优化这件事情,非常复杂
为了稳妥起见,可以给Instance直接加上一个volatile,从根本上杜绝,内存可见性问题
private static volatile SingletonLazy instance = null;
这里更关键的问题是:指令重排序
指令重排序也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码执行的先后顺序,以达到提升性能的效果
编译器优化,往往不只是javac(Java语言的编译器,Java Compile)通常是javac和jvm配合的效果(甚至是操作系统也要配合)
instance = new SingletonLazy();
实例化对象,通常包括以下三个步骤:
-
申请内存空间
-
在空间上构造对象(初始化)
-
内存空间的首地址,赋值给引用变量
正常来说,这三个步骤,是按照1 2 3 这样的步骤来执行的
但是,在指令重排序下,可能是 1 3 2这样的顺序
单线程下1 2 3还是1 3 2 其实无所谓
如果是1 3 2 这样的顺序执行,多线程下 是会出现bug的
对应的解决方法也要用到 volatile,并且上面也碰巧把这个问题解决了:
private static volatile SingletonLazy instance = null;
Volatile 的功能有两方面:
-
确保每次独缺操作,都是读内存 内存可见性
-
关于该变量的读取和修改操作,不会触发重排序