目录
单例模式
饿汉模式
懒汉模式
问题一
问题二
问题三
单例模式
单例模式,是设计模式的一种。在有些特定场景中,有的特定的类,只能创建出一个实例,不应该创建多个实例。单例模式就可以保证这样的需求。例如JDBC中的DataSource就适用于单例模式。常见的实现单例模式的方式有:饿汗模式和懒汉模式。
饿汉模式
使用 Singleton 类来创建对象。
private static Singleton instance = new Singleton();
用 static 来修饰,因此 instance的属性与实例无关,而是与类相关。由于类对象在一个java进程中,只有唯一的一份,因此类对象内部的 类属性 也是唯一的一份。
同时,这个类对象的创建是在类加载过程创建的,类加载对于运行过程来说是比较靠前的阶段,这就给人一种 “十分急切” 的感觉,因此也就称为饿汉模式。(因此也可以理解为饿汉模式也就是创建对象特别早)
(java代码中的每一个类,都会在编译完成后得到唯一的 .class 文件。当 jvm 运行的时候,就会加载这个 .class 文件读取其中的二进制指令,并且在内存中构造出对应的类对象,形如下述代码中的 Singleton.class)
static保证了这个实例的唯一性:
1.static使 instance 属性是类属性,类属性是对类对象而言的,类对象又是唯一实例的(在类加载阶段被创造出的一个实例)
2.构造方法是设为 private,因此外面的代码中无法new。
(运行一个java程序,就需要让java进程能够找到并读取对应的 .class 文件。就会读取文件内容,并解析,构造成类对象.....这一系列的过程,也称为类加载过程。要执行 java 程序的前提就是得把类加载起来)
在保证了这个实例的唯一性的同时,也保证了这个实例在一个比较早的时机被创建。
实际上类对象本身和 static 没有关系,而是类里面使用 static 修饰的成员,会作为类属性。也就相当于这个属性对应的内存空间在类对象里。
// 饿汉模式的 单例模式 的实现
// 此处保证 Singleton 这个类只能创建出一个实例
class Singleton{
//此处先把实例创建出来
private static Singleton instance = new Singleton();
// 如果需要使用这个唯一实例,统一通过 Singleton.getInstance() 来获取
public static Singleton getInstance(){
return instance;
}
// 为了避免 Singleton 类不小心被复制多份出来
// 把构造方法设为 static ,在类外面,就无法通过 new 的方式来创建这个 Singleton 实例了!
private Singleton(){}
}
public class ThreadDemo19 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
在饿汉模式中,如果是多线程调用,只涉及到 “读操作” ,因此是没有线程安全问题的。
懒汉模式
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
从上述代码中,可以看出实例的创建并非是类加载的时候创建了,而是等到真正第一次使用的时候,判断条件:如果instance为null,这个时候才去创建实例。也就是说当不需要使用的时候,就不会创建实例。
这也就是所谓的懒汉模式,虽说是“懒”,但从效率上来看,需要使用的时候才创建,这样的效率是更高的。
问题一
在懒汉模式中,既涉及到 “读操作”,也涉及到 “写操作”,因此是可能存在线程安全问题的。
通过前面的讲解,也可以发现程序实际上是通过多条指令来执行的,所以在懒汉模式中的getInstance方法中,先大致整体分为如下指令:
load:从内存中读取instance的值;
cmp:对 instance的值与 null 进行比较;
若条件满足,则进行new操作;
save:将new的值赋给instance;
以一种线程不安全的例子来讲解:
从中我们就可以看出,这里的线程安全问题,本质上就是读,比较和写操作不是原子的,这就导致了线程t2 读到的值可能是线程t1 还没来得及写的。这也就是脏读问题。
解决办法也就是对这三个操作进行加锁。
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
synchronized (SingletonLazy.class) {
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){}
}
这时候 t2线程load得到的结果就是 线程t1 修改后的结果了,也就不再是null值了,因此不再创建新对象,而是返回现有对象。
问题二
此时的代码,在每次 getInstance 操作的时候都会进行加锁,而加锁操作是有一定开销的;
而实际上,这里的加锁操作只需要针对在 new 出对象之前,才是有意义的。一旦 new 完对象了,后续调用 getInstance ,此时 instance 的值一定是非空的,所以加锁操作是没有必要的,所以可以做出如下优化:如果对象还没创建,就加锁;如果对象已经创建过了,就不用加锁;
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
加锁 if 条件之后,负责判定是否需要加锁,此时就不再是无脑加锁了,也就提高了运行效率。
问题三
此时的代码依旧存在线程安全问题,假设有很多线程,都去进行 getInstance,这个时候,是有可能会被优化的,正如我们之前所讲的情况:只有第一次读才是真正读了内存,后续的读都是读取寄存器/cache,这也就涉及到内存可见性问题了。
除此之外,指令重排序也是会导致线程安全问题的。
instance = new SingletonLazy();
这条语句会有三条指令:
1.申请内存空间;
2.调用构造方法,把这个内存空间初始化成一个合理的对象;
3.把内存空间的地址赋值给 instance 引用;
正常情况下,是按照123的顺序来执行的,但编译器为了提高程序效率,可能就会调整执行顺序,在多线程的环境下,就会可能出现线程安全问题。(单线程环境下没有关系)
例如:假设线程t1 按照 132的顺序来执行,t1线程执行完1 3之后,在执行2 之前,被切出CPU了,这时候线程t2 进行执行,就会发现instance 的引用非空,那么线程t2 就会直接返回instance引用,并且可能会尝试使用 引用的属性。但由于线程t2 此时拿到的是非法的对象,也就是没构造完成的不完整的对象,再去使用的话就会出现线程安全问题。
因此针对这个问题,就需要使用volatile来修饰,volatile 解决两个问题:
1.内存可见性;2.指令重排序;
因此最终优化后的代码为:
//经典面试题,解决多线程安全问题!!!
// 懒汉模式的 单例模式 的实现
class SingletonLazy{
private volatile static SingletonLazy instance = null; //volatile来解决内存可见性,指令重排序
public static SingletonLazy getInstance(){
if(instance == null){ //不再是无脑加锁,而是满足判断是否为空再加锁
synchronized (SingletonLazy.class) { //对类对象进行加锁
if ( instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}