Java多线程之安全发布对象

一、发布对象与对象逸出

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

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

(1)不安全地发布对象

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

这个类通过public级别发布了类的域,在类的任何外部的线程都可以访问这些域。这样发布的对象是不安全的,因为无法假设其他线程会不会修改这个域,从而造成类里面这个状态的错误。

通过new UnsafePublish()发布了这个类的实例,当有了这个实例之后可以通过它提供的public方法直接得到里面的私有域states,就可以在其他任何线程里直接修改这个数组里的值,当我在任何一个线程里面真正想使用states里面数据的时候,它的数据是不完全确定的,这样发布的对象就是线程不安全的。

(2)对象逸出

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

这个内部类(InnerClass)的实例里面包含了对封装实例thisCanBeEscape的隐含的引用,这样在对象没有被正确构造完成之前就会被发布,有不安全的因素在里面,导致this引用在构造期间逸出的错误。

在构造函数public Escape过程中new InnerClass相当于启动了一个线程,无论是隐式的启动还是显式启动,都会造成this引用的逸出。新线程总会在所属对象构造完成之前就看到它了,所以如果要在构造函数中创造线程,那么不要启动它,而是采用start或初始化的方法来统一启动线程。

如果不正确的发布了可变对象,那么会导致两种错误,首先是发布线程以外的任何线程都可以看到被发布对象的过期的值,其次,线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的,如果一个对象是可变对象,那么它要被安全发布才可以。

二、安全发布对象

(一)懒汉模式发布对象


但是这个在单线程的时候没有问题,在多线程环境下就可能出现问题,比如两个线程都访问到19行,都拿到了实例的空值,都会做一次实例化,那么构造函数就会被调用两次,此时两个线程拿到的实例是不一样的。

那么怎么把它变成线程安全呢:

方式一:


但是这个不推荐,原因是一旦加了synchronized之后,它通过只允许同一时间同一线程访问的方式来保证线程安全,但是带来了性能方面的开销。

方式二:(线程不安全)


这是线程不安全的,原因是:

当执行instance=new SingletonExample4()

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

2. ctorInstance()初始化对象

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

在完成三步之后,第19行的instance就指向了它实际分配的内存地址了,就是我们说的引用,在单线程情况下,22行实例化动作之后,上面说的三步执行完了,直接返回(26行),没有问题,但是在多线程情况下,可能会发生指令重排序,指令重排对单线程没有印象,因为123随便排序,不会对返回的instance26行)产生任何影响,但是在多线程情况下,JVMCPU发生了指令重排,上述三步不再按照123进行,而是变成132顺序,因为23本质没有关系,所以是可能发生重排的。

假设有线程AB调用18方法,可能出现:A执行到了22B刚执行到19,按照132顺序,A就是执行了3,而B执行到19之后,发现instance已经存在,就直接return,而实际上,A2还没有做,也就是没有初始化对象,线程B在拿到没有做instance之后,一旦调用就会出现问题。

方式三:使用volatile限制指令重排序


(二)饿汉模式发布对象


由于是类加载的时候实例化,所以是线程安全的。不足就是,如果构造方法中加入过多的处理会导致类加载时很慢。如果使用了饿汉模式,却没有调用的话,会造成资源浪费。

(1)用静态块初始化对象

但要注意,静态域要在静态代码块前面。不然执行到静态代码块有值了,执行到静态域又成了空值。

(三)枚举模式发布对象


我们推荐这个方式,因为相比于懒汉模式安全,相比于饿汉模式,它初始化只在调用的时候。

猜你喜欢

转载自blog.csdn.net/weixin_40459875/article/details/80293117
今日推荐