多线程(初阶六:单例模式)
目录
一、单例模式的简单介绍
二、饿汉模式
三、懒汉模式
四、饿汉模式和懒汉模式的线程安全问题分析
1、饿汉模式(线程安全)
2、懒汉模式(线程不安全)
解决懒汉模式的线程安全问题
①给写操作打包成原子
②去除冗余操作
③存在指令重排序的问题
3、解决懒汉模式线程安全问题的最终代码:
一、单例模式的简单介绍
单例模式是一种设计模式,其中设计模式是软性的规定,与它关联的框架是硬性的规定,这些都是大佬已经设计好了的,即使是代码写的不是很好的菜鸡,按照这种模式也能写出还行的代码。类似象棋中的棋谱,即使你是新手,但按着棋谱走,你的棋力也不会太差。
单例 = 单个实例(对象);某个类,在一个线程中,只应该创建一个对象(原则上不应该有多个),这时就使用单例模式,就可以对我们的代码进行一个更严格的校验和检查。
那么,怎么保证这一个对象唯一呢?
其一方法,可以通过“君子约定”,写一个文档,规定这个类只能有唯一的实例,新手程序猿接手这个代码时,就会发一份这个文档,进行约定,熟悉其中的规定、条约。
其二方法:可以让机器帮我们检查,人肯定是没有机器靠谱的,我们期望让机器帮我们对代码中指定的类,创建类的实例个数进行检查、校验,当创建的实例个数超过我们期望个数,就编译报错,这一点还是能实现的,其中单例模式就是已经设计好的套路,可以实现这种预期效果。
二、饿汉模式
饿汉模式是指创建实例是时期非常早,在类加载的时候,程序一启动,就已经创建好实例了,使用 “饿汉”这个词,就是形容创建实例非常迫切,非常早。下面实现一个单例模式
代码:
class Singleton { private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } private Singleton(){ } } public class TestDemo4 { public static void main(String[] args) { } }
当我们想在主线程中创建一个Singleton的实例时,会报错,如图:
singleton类的代码解析:
singleton内部,第一行代码就是这个,如图
这说明,singleton内部一开始就创建好了实例,创建实例非常迫切,使用static修饰说明instance是类属性。
接下来是获取这个类的实例方法,如图
因为我们不希望能创建出多个实例,所以就把singleton的构造方法用private来修饰,如图:
这样,如果我们想new一个Singleton对象,也new不了,但也有非正规手段,去获取singleton里面的属性或方法:反射。
最后,不管我们用getInstance获取多少次实例,获取的对象都是同一个对象,验证如下:
代码:
class Singleton { private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } private Singleton(){ } } public class TestDemo4 { 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() { } } public class TestDemo5 { public static void main(String[] args) { } }
代码解析:
一开始,没有创建实例,只是给singletonLazy赋值为null,并没有new一个对象,也就是没有创建实例;首次调用getInstance,instance是null的,所以会new一个对象,创建实例。如果不是第一次调用getInstance,就直接返回instance,这也保证了这个类的实例是唯一的,只有一个实例;
和饿汉模式的区别就是没那么迫切创建实例,等需要调用这个类的时候才创建一个实例,而饿汉模式是有了这个类就创建出实例。
懒汉模式的优点:有的程序,要在一定条件下,才需要进行相关的操作,有时候不满足这个条件,也就不需要完成这个操作了,这样,就把这个操作省下来了,而懒汉模式,就是这一思想,当需要这个实例时,才创建实例。像肯德基的疯狂星期四,只有在星期四的时候才会加载出相关信息,其他时间就不会加载。
四、饿汉模式和懒汉模式的线程安全问题分析
1、饿汉模式(线程安全)
代码分析:
当有多个线程,同时并发执行,调用getInstance方法,取instance,这时,线程是安全的吗?显然。这是线程安全操作,因为只涉及到读,多线程读取同一个变量,是线程安全的。而instance很早之前就已经创建好了,不会修改它,一直也只有这一个实例,也不涉及写的操作。
2、懒汉模式(线程不安全)
代码分析:
这里调用getInstance方法后,就会创建出实例来,那么我们想想,如果多个线程同时调用这个方法,此时SingletonLazy类里面的instance都为null,那么这些线程都会new对象,就会创建多个实例,这时,就不符合我们单例模式的预期了,所以,这个代码是线程不安全的。
这也是线程不安全的直接原因,就是 “写” 操作不是原子的。
解决懒汉模式的线程安全问题
①给写操作打包成原子
因为多线程并发执行的时候,可能读到的都是instance == null,所以会创建多个实例,那我们就给它加锁,让它在创建实例的时候,只能创建一个。
代码:
class SingletonLazy { private static Object locker = new Object(); private static SingletonLazy instance = null; public static SingletonLazy getInstance() { synchronized (locker) { if(instance == null) { instance = new SingletonLazy(); } } return instance; } private SingletonLazy() { } }
这样,能让写操作的时候打包成一个原子,实例只可能创建一个的情况。
②去除冗余操作
以上操作加上了还是有问题:如果已经创建出实例了,我们还有加锁来判断它是不是null吗,加锁这些操作也是要消耗硬件资源的,没有必要为此浪费资源空间,如果已经不是null了,我们就想让它直接返回,不再进行加锁操作。
代码:
class SingletonLazy { private static Object locker = new Object(); private static SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null) { synchronized (locker) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() { } }
注意看这里有两个判断条件是一样的,目前位置,也应该是我们第一次遇到这种代码,为什么要套两层一模一样的代码呢?要注意,这里的两个条件表达的都是不同的意思,第一个if条件判断是要让instance==null时,才要加锁的意思,提升代码的执行效率;而第二个if条件判断是把写操作打包成一个原子,保证线程安全。这两个if条件判断所表达的意思是不同的。
③存在指令重排序的问题
指令重排序:指令重排序也是编译器的一种优化,在保证原代码的逻辑不变,调整原代码的指令执行顺序,从而让程序的执行效率提高。
举个栗子:
现在我们要去菜市场买菜,买菜的清单:黄瓜,西红柿,萝卜,茄子,卖这些菜的位置分布图:
如果按照买菜清单顺序买,路线是这样的:
但是我们如果改变顺序,就能缩短我们买菜的时间了,如图:
指令重排序也是类似的道理,保证原代码的逻辑不变,改变原有指令的顺序,从而提高代码的执行效率,其中这个代码,就存在着指令重排序的优化,如图:
原本指令执行顺序:
1、去内存申请一段空间
2、在这个内存中调用构造方法,创建实例
3、从内存中取出地址,赋值给这个实例instance。
指令重排序后的顺序:1, 3 , 2
按照指令重排序后的代码执行逻辑就变成了这样:
假设有两个线程,现在执行顺序是这样的,如图:
因为指令重排序后,先去内存申请一段空间,然后是赋值给instance,那这时,instance就不是null了,第二个线程不会进入到if语句了,直接返回instance,可是instance还没有创建出实例,这样返回肯定是有问题的,这样,也就线程不安全了。
解决方案:给instance这个变量,加volatile修饰,强制取消编译器的优化,不能指令重排序,同时也排除了内存可见性的问题。
加volatile后的代码:
class SingletonLazy { private static Object locker = new Object(); private static volatile SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null) { synchronized (locker) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() { } }
到这一步,才真的解决了懒汉模式的线程安全问题。
3、解决懒汉模式线程安全问题的最终代码:
class SingletonLazy { private static Object locker = new Object(); private static volatile SingletonLazy instance = null; public static SingletonLazy getInstance() { if (instance == null) { synchronized (locker) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() { } } public class ThreadDemo1 { public static void main(String[] args) { SingletonLazy s1 = SingletonLazy.getInstance(); SingletonLazy s2 = SingletonLazy.getInstance(); System.out.println(s1 == s2); } }
main线程是来测试每次拿到SingletonLazy的实例是不是同一个,
执行结果:符合我们的预期