Java 并发系列之十三:安全发布

1. 定义

  • 发布对象(Publish): 使一个对象可以被当前范围以外的代码所使用
  • 对象逸出(Escape): 一种错误的发布。当一个对象尚未构造完成时,就使它被其余线程所见

1.1 发布对象

public class UnsafePublish {
    private String[] states = {"a","b","c"};
    public String[] getStates(){
        return states;
    }

    /**
     * 经过new UnsafePublish()发布了一个UnsafePublish类的实例
     * 经过实例的public方法获得了私有域states数组的引用
     * 能够在其余任何线程里修改这个数组里的值
     * 这样在其余线程中想使用states数组时,它的值是不彻底肯定的
     * 所以这样发布的对象是线程不安全的,由于没法保证是否有其余线程对数组里的值进行了修改
     * @param args
     */
    public static void main(String[] args) {
        UnsafePublish unsafePublish = new UnsafePublish();
        for (String i : unsafePublish.getStates()) {
            System.out.print(i+" ");
        }
        unsafePublish.getStates()[0] = "d";
        System.out.println();
        for (String i : unsafePublish.getStates()) {
            System.out.print(i+" ");
        }
    }
}

 

1.2 对象逸出

@NotThreadSafe
public class Escape {
    private int thisCanBeEscape = 0;
    public Escape(){
        new InnerClass();
    }

    /**
     * 内部类的实例里面包含了对封装内容thisCanBeEscape的隐含引用
     * 这样在对象没有被正确构造以前,他就会被发布,有可能有不安全的因素在
     * 一个致使this引用在构造期间逸出的错误   是在构造的函数过程当中启动了一个线程
     * 不管是隐式的启动仍是显示地启动都会形成this引用的逸出,新线程老是会在对象构造完毕
     * 以前就已经看到this引用    因此要再构造函数中使用线程,就不要启动它而应该专有的start或初始化的方法来统一启动线程,
     * 能够采用工厂方法和私有构造函数来完成对象建立和监听器的注册等
     *
     *
     * 在对象未完成构造以前   不能够将其发布
     */
    private class InnerClass{
        public InnerClass(){
            System.out.println(Escape.this.thisCanBeEscape);
        }
    }

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

 

 

2. 问题(引用+状态,构造函数+正确发布)

不正确的发布可变对象致使的两种错误:html

一、发布线程以外的全部线程均可以看到被发布对象的过时的值【引用过时】
二、线程看到的被发布对象的引用是最新的,然而被发布对象的状态倒是过时的【状态过时】编程

正确发布一个对象遇到的两个问题:数组

  (1)引用自己要被其余线程看到;安全

  (2)对象的状态要被其余线程看到。多线程

  ps: 在多线程编程中,首要的原则,就是要避免对象的共享,由于若是没有对象的共享,那么多线程编写要轻松得多,可是,若是要共享对象,那么除了可以正确的将构造函数书写正确外,如何正确的发布也是一个很重要的问题。函数

  

public class Client {
    public Holder holder;
    
    public void initialize(){
        holder = new Holder(42);//这个代码不是原子的
    }
}

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

/**
    在Client类中,Holder对象被发布了,可是这是一个不正确的发布。因为可见性问题,其余线程看到的Holder对象将处于不一致的状态,即便在该对象的构成构函数中已经正确的该构建了不变性条件,这种不正确的发布致使其余线程看到还没有建立完成的对象。主要是Holder对象的建立不是原子性的,可能还未构造完成,其余线程就开始调用Holder对象。

    因为没有使用同步的方法来却确保Holder对象(包含引用和对象状态都没有)对其余线程可见,所以将Holder成为未正确发布。问题不在于Holder自己,而是其没有正确的发布。上面没有正确发布的可能致使的问题:

    别的线程对于holder字段,可能会看到过期的值,这样就会    致使空引用,或者是过期的值(即便holder已经被设置了)(引用自己没有被别的线程看到)
更可怕的是,对于已经更新holder,及时可以看到引用的更新,可是对于对象的状态,看到的却多是旧值,对于上面的代码,可能会抛出AssertionError异常
主要是holder = new Holder(42);这个代码不是原子性的,可能在构造未完成时,其余线程就会调用holder对象引用,从而致使不可预测的结果。
*/

 

3. 安全对象的构造过程

不要在构造函数内显式或者隐式的的公布this引用。post

(1)在对象构造期间,不要公布this引用this

  若是要在构造函数中建立内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工做已所有完成,再发布内部类。url

public class EventListener { 
    public EventListener(EventSource eventSource) { 
        // do our initialization ... 
        // register ourselves with the event source 
        eventSource.registerListener(this); 
    } 
    public onEvent(Event e) { // handle the event } 
} 
public class RecordingEventListener extends EventListener { 
    private final ArrayList list; 
    public RecordingEventListener(EventSource eventSource) { 
        super(eventSource); 
        list = Collections.synchronizedList(new ArrayList()); 
    } 
    public onEvent(Event e) { 
        list.add(e); 
        super.onEvent(e); 
    } 
    public Event[] getEvents() { 
        return (Event[]) list.toArray(new Event[0]); 
    }  
} 

  

(2)不要隐式地暴露“this”引用spa

public class EventListener2 { 
    public EventListener2(EventSource eventSource) { eventSource.registerListener( 
            new EventListener() { 
                public onEvent(Event e) { 
                    eventReceived(e); 
                } 
     }); 
    } 
    public eventReceived(Event e) { } 
} 
一样也是子类化问题

(3)不要从构造函数内启动线程
     a)在构造函数中启动线程时,构造函数还未执行完毕,不能保证此对象已经彻底构造
     b)若是在启动的线程中访问此对象,不能保证访问到的是彻底构造好的对象

 

3. 安全发布经常使用模式

要安全的发布一个对象,对象的引用和对象的状态必须同时对其余线程可见。通常一个正确构造的对象(构造函数不发生this逃逸),能够经过以下方式来正确发布:

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

  (2)将一个对象引用保存在volatile类型的域或者是AtomicReference对象中

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

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

/**

   不变性: 某个对象在被建立后其状态就不能被修改,那么这个对象就称为不可变对象,不可变对象必定是线程安全的。不可变对象很简单。他们只有一种状态,而且该状态由构造函数来控制。

  当知足如下条件时,对象才是不可变的:

    1)对象建立之后其状态就不能改变;

    2)对象的全部域都是final类型;

    3)对象是正确创造的(在对象建立期间,this引用没有溢出)。

(1)Java中存在三种对象
   a)不变对象:对象状态建立后不能再修改,对象的全部域为final,对象是正确构造的
   b)基本不变对象:不知足不变对象的约束,可是初始化后再也不变化
   c)可变对象:不知足上述不变对象和基本不变对象的约束

(2)安全发布技术
   a)即确保对象引用和状态对其余线程正确可见
   b)方式
      静态初始化器初始化对象引用
      将引用存储到volatile域
      将引用存储到正确建立对象的final域
      将引用存储到由锁正确保护的域

(3)三种对象安全发布方式
   a)不变对象:任何形式机制发布
   b)基本不变对象:保证安全发布便可
   c)可变对象:不只要保证安全发布,并且要确保对象状态的正确改变(即用锁或其余方式,保证对象状态的正确改变)

  一般,要发布一个静态构造的对象,最简单和最安全的方式是使用静态初始化器: public static Holder = new Holder(42);

  静态初始化器由JVM在类的初始化阶段执行,因为JVM内部存在同步机制,因此这种方式初始化对象均可以被安全的发布。

  对于可变对象,安全的发布之时确保在发布当时状态的可见性,而在随后的每次对象的访问时,一样须要使用同步来确保修改操做的可见性。(状态可见性+同步)

**/

 

4. 容器安全发布保证

  在线程安全容器内部同步意味着,在将对象放到某个容器中,好比Vector中,将知足上面的最后一条需求。若是线程A将对象X放到一个线程安全的容器中,随后线程B读取这个对象,那么能够确保能够确保B看到A设置的X状态,即使是这段读/写X的应用程序代码没有包含显示的同步。下面容器内提供了安全发布的保证:

  (1)经过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,能够安全将它发布给任何从这些容器中访问它的线程。

  (2)经过将某个元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,能够将该元素安全的发布到任何从这些容器中访问该元素的线程。

  (3)经过将元素放到BlockingQueue或者是ConcrrentLinkedQueue中,能够将该元素安全的发布到任何从这些访问队列中访问该元素的线程。

  

5. 网址

  1. 安全发布对象(一)

  2. Java多线程——volatile关键字、发布和逸出

  3. Java多线程——不变性与安全发布

  4. 第三章 对象的共享(三)

相关文章
相关标签/搜索