如何安全的发布对象?

简介: 如何安全的发布对象?

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


与之对应的一个问题是 对象逸出


对象逸出是一种错误的发布对象方式,当一个对象还没有构造完时,就被其他线程所见。常见于多线程之中。


错误的发布对象:


私有成员变量在对象的公有方法中被修改。当其他线程访问该私有变量时可能得到不正确的值。


例如:

  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()));

对象逸出的例子:


在这个内部类中,有对封装对象的私有成员变量的引用。在对象没有被正确完成构造之前,它就会被发布。不安全的因素就是在构造函数中显示的启动了一个线程,不管是显示还是隐式的启动,都会造成this引用的逸出,新线程在对象完成构造之前就看到了。


在构造函数中不要直接构造对象,其实就是创建了一个线程,存在上述的逸出风险,


如果要在构造函数中创建线程,应该用一个专有的start或初始化的方法来统一启动线程。可以采用 工厂方法 和私有构造函数解决。

总之在对象未完成之前不能将其发布,是安全发布的准则。否则其他线程可能会看到旧值,

public 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();
    }
}

如何安全的发布的?


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

2、将对象的引用保存在volatile类型域中

3、将对象的引用保存在某个正确构造的final类型域中。

4、将对象的引用保存咋一个y由锁保护的域中。


下面是一个用静态工厂方法线程不安全的例子


这种方式称为懒汉模式

单例实例在第一次使用时进行创建

该段代码在单线程中运行是没有问题的,但是在多线程中会存在问题,例如两个线程都开始访问这个方法时就会出现该实例被实例化两次,如果实例化过程中有逻辑运算则返回不同的值。

    // 私有构造函数
    private SingletonExample1() {
    }
    // 单例对象
    private static SingletonExample1 instance = null;
    // 静态的工厂方法
    public static SingletonExample1 getInstance() {
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }

那么如何保证在多线程中保证呢?

饿汉模式:

单例实例在类装载时进行创建.

因此使用饿汉模式的时候需要注意


1、构造私有函数的时候没有太多的处理。

2、这个类被加载后肯定会被使用。

3、如果使用静态域和静态代码块初一定要注意 静态域和静态代码块的顺序.(静态域在静态代码块之前)

// 私有构造函数
private SingletonExample2() {
}
// 单例对象
private static SingletonExample2 instance = new SingletonExample2();
// 静态的工厂方法
public static SingletonExample2 getInstance() {
    return instance;
}
上面的方法也可以用过静态带啊模块来实现实例化
    // 私有构造函数
    private SingletonExample6() {
    }
    // 单例对象
    private static SingletonExample6 instance = null;
    static {
        instance = new SingletonExample6();
    }
    // 静态的工厂方法
    public static SingletonExample6 getInstance() {
        return instance;
    }
    public static void main(String[] args) {
        System.out.println(getInstance().hashCode());
        System.out.println(getInstance().hashCode());
    }

深究一下 懒汉模式时候是否可以实现线程安全呢?


使用synchronized关键字。


该方法 同一时间内只允许一个线程访问,是可以保证线程安全的,但是牺牲了效能 ,竞争激烈的时候会有很多线程wait

    // 私有构造函数
    private SingletonExample3() {
    }
    // 单例对象
    private static SingletonExample3 instance = null;
    // 静态的工厂方法
    public static synchronized SingletonExample3 getInstance() {
        if (instance == null) {
            instance = new SingletonExample3();
        }
        return instance;
    }

synchronized修饰方法所造成的的效能问题是否可以再优化呢?


但是可以的,可以将synchronized下沉到方法实现里面。代码如下:在判断完之后单独锁定该类(也就是开始说到如何保证线程安全的第四点)通常称其为 双重同步锁单例模式。


这个类是线程安全的吗?


并不是,


instance = new SingletonExample4();


当执行完上面这行代码时需要三步:

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

   // 2、ctorInstance() 初始化对象

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


在单线程下上面逻辑是线程安全的,但是多线程下会发生指令重排


   // JVM和cpu优化,发生了指令重排


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

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

   // 2、ctorInstance() 初始化对象


在指令重排后发生的对象未完成初始化的动作时,却被直接return。

    // 私有构造函数
    private SingletonExample4() {
    }
    // 单例对象
    private static SingletonExample4 instance = null;
    // 静态的工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample4.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }

如何避免其指令重排呢?


使用关键字volatile 和双重检查机制可以禁止指令重排。

    // 私有构造函数
    private SingletonExample5() {
    }
    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存
    // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
    private volatile static SingletonExample5 instance = null;
    // 静态的工厂方法
    public static SingletonExample5 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample5.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample5(); // A - 3
                }
            }
        }
        return instance;
    }
目录
相关文章
|
C# C++
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
1693 0
创建目标类型对象在C#7.3中不可用,请使用9.0或更高的语言版本
|
安全
并发编程-08安全发布对象之发布与逸出
并发编程-08安全发布对象之发布与逸出
51 0
|
缓存
读源码长知识 | 动态扩展类并绑定生命周期的新方式
在阅读viewModelScope源码时,发现了一种新的方式。 协程需隶属于某 CoroutineScope ,以实现structured-concurrency,而 CoroutineScope 应
173 0
|
开发者 Python
对象的创建流程|学习笔记
快速学习 对象的创建流程
125 0
对象的创建流程|学习笔记
使用Unity获取所有子对象及拓展方法的使用
这个问题还是比较简单的,无非就是一个for循环就可以全部获取到了,但是我喜欢简单直达,有没有直接就能获取到所有的子对象函数呢,搜了好久都没有,所以我准备写一个扩展函数,来自己补充这个函数,一起来看一下吧。