【Java EE初阶六】多线程案例(单例模式)
1. 单例模式
单例模式是一种设计模式,设计模式是我们必须要掌握的一个技能;
1.1 关于框架和设计模式
设计模式是软性的规定,且框架是硬性的规定,这些都是技术大佬已经设计好的;
一般来说设计模式有很多种,且不同的语言会有不同的设计模式,(同时设计模式也可以理解为对编程语言的一种补充)
1.2 细说单例模式
单例 = 单个实例(对象);
某个类,在一个线程中,只应该创建一个实例化对象(原则上不应该有多个),这时就使用单例模式,如此可以对我们的代码进行一个更严格的校验和检查。
保证对象唯一性的方法:
方法一,可以通过“协议约束”,写一个文档,规定这个类只能有唯一的实例,程序员在接手这个代码时,就会发现这个文档已经进行约定,其中的规定约束着程序员在创建对象时,时刻注意只能创建一个对象。
方法二:从机器入手;让机器帮我们检查,我们期望让机器帮我们对代码中指定的类,创建类的实例个数进行检查、校验,当创建的实例个数超过我们期望个数,就编译报错。其中单例模式就是已经设计好的套路,可以实现这种预期效果。
关于单例模式代码实现的基本方式有两种:饿汉模式和懒汉模式;
2. 饿汉模式
饿汉模式是指创建实例的时期非常早;在类加载的时候,程序一启动,就已经创建好实例了,使用 “饿汉”这个词,就是形容创建实例非常迫切,非常早。单例模式代码如下:
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 = new Singleton();
}
}
当我们运行该代码时,系统就会报错,接下来我们详细的分析一下此处的代码;
这样,如果我们想new一个Singleton对象,也new不了,同时不管我们用getInstance获取多少次实例,获取的对象都是同一个对象,代码如下:
package thread;
// 就期望这个类只能有唯一的实例 (一个进程中)
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {}
}
public class ThreadDemo26 {
public static void main(String[] args) {
// Singleton s = new Singleton();
Singleton s = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s == s2);
}
}
结果如下:
3. 懒汉模式
和饿汉模式不一样的是,懒汉模式创建实例的时机比较晚,没饿汉创建实例那么迫切,只有第一次使用这个类时,才会创建实例,代码如下:
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) {
}
}
下面为代码图解分析:
和饿汉模式的区别就是没那么迫切创建实例,等需要调用这个类的时候才创建一个实例,而饿汉模式是有了这个类就创建出实例。
懒汉模式的优点:有的程序,要在一定条件下,才需要进行相关的操作,有时候不满足这个条件,也就不需要完成这个操作了,如此哦·就把这个操作省下来了。
4. 两种模式关于线程安全
4.1 饿汉模式
线程安全;
对于饿汉模式来说,上图所示通过调用getinstance方法来返回instance对象,本质上来说是读操作;
当有多个线程,同时并发执行,调用getInstance方法,取instance,这时线程是安全的,因为只涉及到读,多线程读取同一个变量,是线程安全的。而instance很早之前就已经创建好了,不会修改它,一直也只有这一个实例,也不涉及写的操作。
4.2 懒汉模式
线程不安全;
在懒汉模式中,条件判定和返回时是读操作,new一个对象是写操作;
我们只有调用getInstance方法后,就会创建出实例来,如果多个线程同时调用这个方法,此时SingletonLazy类里面的instance都为null,那么这些线程都会new对象,就会创建多个实例。这时,就不符合我们单例模式的预期了,所以,这个代码是线程不安全的。
线程不安全的直接原因,就是 “写” 操作不是原子的。
4.3 解决懒汉模式的线程安全问题
4.3.1 把写操作打包成原子
因为多线程并发执行的时候,可能读到的都是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() { }
}
以上操作虽然将写操作打包成了一个原子,但是新的问题也出现了;
4.3.2 去除冗余操作
上述操作加上了还是有问题:如果已经创建出实例了,我们还有加锁来判断它是不是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() { }
}
代码图解分析两个判断语句的是目的意义:
4.3.3 指令重排序的问题
指令重排序:指令重排序也是编译器的一种优化,在保证原代码的逻辑不变,调整原代码的指令执行顺序,从而让程序的执行效率提高。
保证原代码的逻辑不变,改变原有指令的顺序,从而提高代码的执行效率,其中这个代码,就存在着指令重排序的优化,如下图代码:
该语句原本指令执行顺序:
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() { } }
至此,我们才算解决掉懒汉模式关于线程安全的所有问题;
4.4 懒汉模式线程安全的代码
package thread;
// 懒汉的方式实现单例模式.
class SingletonLazy {
// 这个引用指向唯一实例. 这个引用先初始化为 null, 而不是立即创建实例
private volatile static SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance() {
// 如果 Instance 为 null, 就说明是首次调用, 首次调用就需要考虑线程安全问题, 就要加锁.
// 如果非 null, 就说明是后续的调用, 就不必加锁了.
if (instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
public class ThreadDemo27 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
结果如下:
ps:本次的内容就到这里了,如果感兴趣的话,就请一键三连哦!!!