如何安全的发布对象?

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

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

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

错误的发布对象:

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

例如:

  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;
    }

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

饿汉模式:

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

由于是类装载时就就创建实例,因此是线程安全的。不过需要注意的是:如果构造方法中有过多的处理,会导致类加载时特别慢。使用饿汉模式只进行加载如果没有实际调用的话会耗费很多资源。(思考一下,为什么有的项目启动需要2-3mins,这过程中加载了多少实例?需要多久?)

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

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;
    }

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MyySophia

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值