不可变对象与this逃逸

不可变对象

    不可变对象是不需要进行任何的同步措施就可以保证其安全性。判断一个对象是不是线程安全的最难的就是判断它的状态,在上一节中解释了影响一个对象对其他线程可见性因素,因此要保证我们发布的可变对象对其他线程可见的话必须要进行同步措施。但是如果我将要发布的对象是一个不可变对象我们很容易就能判断它的状态而不需要进行任何同步。

public class Holder{
    private int n;

    public Holder(int n){
        this.n = n;
    }
    public void assertSanity(){
        if(n != n){
            throw new AssertionError("This statement is false");
        }
    }
}

    这是一段我们很熟悉的代码,在了解JMM内存模型之前我们并没有发现这段代码可疑的地方,但是经过JMM学习之后我们在考虑问题的时候如果加上可见性的约束,问题的答案好像就不那么显而易见了。当我在对象构造完成之后多个线程调用assertSanity方法时由于我们没有考虑可见性的问题,两次读到的值不一样,可能会抛出AssertionError异常。但是如果n使用final修饰的话JMM会保证其可见性,即使对象并没有正确被发布也不会抛出AssertionError异常。

This逃逸

    this逃逸通俗来讲就是当对象并没有正确构造完成但是已经被不正确发布。

public class TestThis {
    private final int i;

    public TestThis(int i){
        new Thread(()->PrintDemo.print(this)).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.i = i;
        new Thread(()->PrintDemo.print(this)).start();
    }

    static class PrintDemo{
        public static void print(TestThis testThis){
            System.out.println(testThis.i);
        }
    }

    public static void main(String[] args) {
        new TestThis(1);
    }
}

    这段代码不难看懂,我在构造对象的时候在构造器内加了两个线程,并把该类的this引用传递了出去,这就会导致我们在使用该对象的时候出现了访问不一致的情况。访问的结果第一个线程拿到 i 的值是0,而第二个线程拿到 i 的值是 1 。因此绝对不要在构造器内将 this 的引用传递出去。

如何正确的发布一个对象

    讲了很多怎么样发布对象是不安全的,那么我们应该怎样做才能正确的发布一个对象呢?给出以下四种解决方案:

    使用静态初始化

    静态初始化应该不用过多解释,当类加载的时候进行初始化保证其可见性,这样做的缺点是当无法保证一定会使用到该对象的时候会产生空间浪费。

    使用 volatile 关键字

    对于volatile关键字之后会做详细说明,它包括了禁止指令重排序与保证可见性这两种功能,CopyOnWriteArrayList就是根据这个关键字实现的,我们来翻一下它的源码

private transient volatile Object[] array;

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

final Object[] getArray() {
        return array;
    }

 final void setArray(Object[] a) {
        array = a;
    }

    简单来看一下,它的底层是用一个volatile修饰的数组来实现的,来看一下它的add方法,他并不像ArrayList一样在原数组上直接进行修改 ,而是将数组进行复制在副本上进行修改,最后将引用指向新数组从而实现读写分离保证其安全性,而新数组对其他线程的可见性就是用volatile实现的。使用volatile的缺点是他的读与写与普通变量相比会产生更多的性能消耗。但相比synchronized来说消耗较少。

    使用线程安全的容器类

    使用线程安全的容器类例如Vector,这个就是典型的经过显示同步来保证其可见性的体现 。

    使用 final 关键字

有不足之处请多指正  转载请注明出处,谢谢

猜你喜欢

转载自blog.csdn.net/qq_37490665/article/details/81269799