写点什么

从安全和不安全两个角度,教你如何发布对象(含各种单例代码)

  • 2022 年 4 月 19 日
  • 本文字数:4578 字

    阅读完需:约 15 分钟

本文分享自华为云社区《【高并发】如何安全的发布对象(含各种单例代码分析)》,作者: 冰 河 。


今天,为大家带来一篇有技术含量的文章,那就是在高并发环境下如何安全的发布对象实例。


  • 发布对象:使一个对象能够被当前范围之外的代码所使用

  • 对象溢出:是一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见

不安全的发布示例代码:

package io.binghe.concurrency.example.publish;import lombok.extern.slf4j.Slf4j;import java.util.Arrays;
@Slf4jpublic class UnsafePublish { private String[] states = {"a", "b", "c"}; public String[] getStates(){ return states; }
public static void main(String[] args){ UnsafePublish unsafePublish = new UnsafePublish(); log.info("{}", Arrays.toString(unsafePublish.getStates()));
unsafePublish.getStates()[0] = "d"; log.info("{}", Arrays.toString(unsafePublish.getStates())); }}
复制代码

其中,每个线程都能获取到 UnsafePublish 类的私有成员变量 states,并修改 states 数组的元素值,造成其他线程获取的 states 元素值不确定。

对象溢出示例代码:

package io.binghe.concurrency.example.publish;import lombok.extern.slf4j.Slf4j;
@Slf4jpublic class Escape { private int thisCanBeEscape = 0;
public Escape(){ new InnerClass(); }
private class InnerClass{ public InnerClass(){ log.info("{}", Escape.this.thisCanBeEscape); } }
public static void main(String[] args){ new Escape(); }}
复制代码

其中,内部类 InnerClass 的构造方法中包含了对封装实例 Escape 的隐含的引用(体现在 InnerClass 的构造方法中引用了 Escape.this),在对象没有被正确构造完成之前,就会被发布,有可能存在不安全的因素。


一个导致 this 在构造期间溢出的错误:在构造函数中,启动一个线程,无论是隐式的启动还是显式的启动,都会造成 this 引用的溢出(因为新线程总是在所属对象构造完毕之前就已经看到 this 引用了)。所以,如果要在构造函数中创建线程,则不要在构造函数中启动线程,可以使用一个专有的 start()方法或者一个初始化方法,来统一启动线程,可以采用工厂方法和私有构造函数来完成对象的创建和监听器的注册,之后统一启动线程,来避免溢出。


注意:在对象未构造完成之前,不可以将其发布

如何安全的发布对象:

(1)在静态初始化函数中初始化一个对象引用

(2)将对象的引用保存到 volatile 类型域或者 AtomicReference 对象中

(3)将对象的引用保存到某个正确构造对象的 final 类型域中

(4)将对象的引用保存到一个由锁保护的域中


接下来,看几个单例对象的示例代码,其中有些代码是线程安全的,有些则不是线程安全的,需要大家细细品味,这些代码在高并发环境下测试验证过的。

代码一:SingletonExample1

这个类是懒汉模式,并且是线程不安全的

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程不安全的 */public class SingletonExample1 {
private SingletonExample1(){}
private static SingletonExample1 instance = null;
public static SingletonExample1 getInstance(){ //多个线程同时调用,可能会创建多个对象 if (instance == null){ instance = new SingletonExample1(); } return instance; }}
复制代码

代码二:SingletonExample2

饿汉模式,单例实例在类装载的时候进行创建,是线程安全的。

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 饿汉模式,单例实例在类装载的时候进行创建,是线程安全的 */public class SingletonExample2 {
private SingletonExample2(){}
private static SingletonExample2 instance = new SingletonExample2();
public static SingletonExample2 getInstance(){ return instance; }}
复制代码

代码三:SingletonExample3

懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推荐。

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推荐 */public class SingletonExample3 {
private SingletonExample3(){}
private static SingletonExample3 instance = null;
public static synchronized SingletonExample3 getInstance(){ if (instance == null){ instance = new SingletonExample3(); } return instance; }}
复制代码

代码四:SingletonExample4

懒汉模式(双重锁同步锁单例模式),单例实例在第一次使用的时候进行创建,但是,这个类不是线程安全的!!!!!

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 懒汉模式(双重锁同步锁单例模式) *              单例实例在第一次使用的时候进行创建,这个类不是线程安全的 */public class SingletonExample4 {
private SingletonExample4(){}
private static SingletonExample4 instance = null;
//线程不安全 //当执行instance = new SingletonExample4();这行代码时,CPU会执行如下指令: //1.memory = allocate() 分配对象的内存空间 //2.ctorInstance() 初始化对象 //3.instance = memory 设置instance指向刚分配的内存 //单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。 // 指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。
//如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行: //1.memory = allocate() 分配对象的内存空间 //3.instance = memory 设置instance指向刚分配的内存 //2.ctorInstance() 初始化对象 //假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new SingletonExample4(); B线程刚执行到第一个 if (instance == null){处, //如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance; //而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。 public static SingletonExample4 getInstance(){ if (instance == null){ synchronized (SingletonExample4.class){ if(instance == null){ instance = new SingletonExample4(); } } } return instance; }}
复制代码

线程不安全分析如下:

当执行 instance = new SingletonExample4();这行代码时,CPU 会执行如下指令:

1.memory = allocate() 分配对象的内存空间

2.ctorInstance() 初始化对象

3.instance = memory 设置 instance 指向刚分配的内存


单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。


指令重排序对单线程没有影响,单线程下 CPU 可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。


如果发生了 JVM 和 CPU 优化,发生重排序时,可能会按照下面的顺序执行:

1.memory = allocate() 分配对象的内存空间

3.instance = memory 设置 instance 指向刚分配的内存

2.ctorInstance() 初始化对象


假设目前有两个线程 A 和 B 同时执行 getInstance()方法,A 线程执行到 instance = new SingletonExample4(); B 线程刚执行到第一个 if (instance == null){处,如果按照 1.3.2 的顺序,假设线程 A 执行到 3.instance = memory 设置 instance 指向刚分配的内存,此时,线程 B 判断 instance 已经有值,就会直接 return instance;而实际上,线程 A 还未执行 2.ctorInstance() 初始化对象,也就是说线程 B 拿到的 instance 对象还未进行初始化,这个未初始化的 instance 对象一旦被线程 B 使用,就会出现问题。

代码五:SingletonExample5

懒汉模式(双重锁同步锁单例模式)单例实例在第一次使用的时候进行创建,这个类是线程安全的,使用的是 volatile + 双重检测机制来禁止指令重排达到线程安全。

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 懒汉模式(双重锁同步锁单例模式) *              单例实例在第一次使用的时候进行创建,这个类是线程安全的 */public class SingletonExample5 {
private SingletonExample5(){}
//单例对象 volatile + 双重检测机制来禁止指令重排 private volatile static SingletonExample5 instance = null;
public static SingletonExample5 getInstance(){ if (instance == null){ synchronized (SingletonExample5.class){ if(instance == null){ instance = new SingletonExample5(); } } } return instance; }}
复制代码

代码六:SingletonExample6

饿汉模式,单例实例在类装载的时候(使用静态代码块)进行创建,是线程安全的。

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 饿汉模式,单例实例在类装载的时候进行创建,是线程安全的 */public class SingletonExample6 {
private SingletonExample6(){}
private static SingletonExample6 instance = null;
static { instance = new SingletonExample6(); }
public static SingletonExample6 getInstance(){ return instance; }}
复制代码

代码七:SingletonExample7

枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的。

package io.binghe.concurrency.example.singleton;/** * @author binghe * @version 1.0.0 * @description 枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的 */public class SingletonExample7 {
private SingletonExample7(){}
public static SingletonExample7 getInstance(){ return Singleton.INSTANCE.getInstance(); }
private enum Singleton{ INSTANCE; private SingletonExample7 singleton;
//JVM保证这个方法绝对只调用一次 Singleton(){ singleton = new SingletonExample7(); } public SingletonExample7 getInstance(){ return singleton; } }}
复制代码

点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
从安全和不安全两个角度,教你如何发布对象(含各种单例代码)_安全_华为云开发者社区_InfoQ写作社区