Java多线程之构造与发布

资料来源

http://www.ibm.com/developerworks/library/j-jtp0618/
http://www.javaspecialists.eu/archive/Issue192.html
http://stackoverflow.com/questions/1621435/not-thread-safe-object-publishing

对象构造函数要做到线程安全

竞态条件

首先看看竞态条件的定义:

data race, or race condition, occurs when multiple threads or processes are reading and writing a shared data item, and the final result depends on the order in which the threads are scheduled。

也就是说,当有多个线程的时候,其中有线程写,并且另外有线程读,那么就会发生竞态条件,如果是多个线程都是读,那么就没有竞态条件(race condition)一个数据竞争的例子:

public class DataRace {
  static int a = 0;

  public static void main() {
    new MyThread().start();
    a = 1;
  }

  public static class MyThread extends Thread {
    public void run() { 
      System.out.println(a);
    }
  }
}

上面由于线程调度的规则问题,使得存在race condition,实际上这里还有一个问题就是因为没有同步操作,存在可见性问题。

发布和逸出的定义

发布一个对象的意思指:是指对象能够在当前作用域之外的代码中使用。例如:

  • 将一个指向该对象的引用保存到其他代码能够访问的地方(非常重要)
  • 在某一个非私有的方法中返回该引用(非常重要)
  • 将该引用传递到其它类的方法。(非常重要)

比如第一种情况对应的发布是:

public class Test {
    // 所有的类都共享,可以在别的作用域中使用
    public static Integer n;
}

第二种情况是:

public class Test {
    prvivate String[] test = new String[] {
        "Hello",
        "World",
    };
    public String[] getHelloOrWorld(){
       return test;
   }
}

第三种情况:

// 在构造函数中引入race condition
public class EventListener { 
  public EventListener(EventSource eventSource) {
    // do our initialization
    ...

    // register ourselves with the event source
    // 将自己发布给eventSource
    eventSource.registerListener(this);
  }

  public onEvent(Event e) { 
    // handle the event
  }
}

逸出:当某个不应该发布的对象被发布的时候,这种情况就叫做逸出。

不要在构造期间发布this引用

一个构建线程安全类构造器的技巧,就是不要在构造器中发布this。也就是在构造器中,不要将this暴露给另外一个线程。有时这个过程是很明显的,比如你将this存到静态域中,或者是一个集合中,有时候是不明显的,你可能将一个一个非静态内部对象中实例在构造器中发布。构造器不是普通的方法,对于初始化的安全性,它们有着特殊的语义。一个对象被构造完成后,就应该认为是处于一致的,可预测的状态。如果发布一个没有完全构造完的对象,那么就会处于危险的状态。 比如下面的例子就引入了race condition。

当且仅当对象构造函数调用完成后,我们才认为对象处于一致的状态。

// 在构造函数中引入race condition
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
  }
}

事件监听器在最后一个将自己暴露给事件源(因为EventSource eventSource对于别的线程是可能是可见的,所以别的线程可能看到了不完整的构造对象),看起来没有什么错误,实际上在不考虑重排序和可见性等问题,这段代码仍然有暴露不完整的EventListener对象的给别的线程。下面,我们将继承EventListener。

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

由于JLS要求子类的构造函数在第一句中使用super来调用父类的构造函数,我们还没有构造完成的事件监听器就已经注册事件了(因为先执行父类的构造函数,在构造函数中调用了注册监听器的功能)。现在对于list字段就存在race condition。如果在这个时候发生了一个时间,在onEvent将会得到调用,那么list可能为null,这样就会抛出nullPointerException 异常。因为对于onEvent是没有必要检查list为空的情况,因为其是一个final的字段,我们假定构造完成后,那么它就应该正确的初始化了。

不要隐式的暴露this引用

也有可能在显式的使用this的情况下,暴露了this引用。非静态的内部类,也包含了this引用的复制品,如果创建里一个匿名内部类对象,并且将该对象暴露给其他线程,那么就和暴露this引用是样。

public class EventListener2 {
  public EventListener2(EventSource eventSource) {

    eventSource.registerListener(
      new EventListener() {
        public void onEvent(Event e) { 
          eventReceived(e);
        }
      });
  }

  public void eventReceived(Event e) {
  }
}

这样EventListen2类和EventListen有同样的毛病,同样this引用被publish。

不要在构造器中启动线程

在上面的代码中,出现上述问题的一个特殊情况是在构造器中启动线程,因为当一个对象拥有线程时候,要么线程是这个对象的内部类,或者我们将this传递给Thread的构造函数。在这种情况下,我们应该提供一个start()方法,通过start()方法来启动线程,而不是在构造函数中启动。

publish的意思

不是所有在构造器中对this的引用都是有害的,只有那些可以对别的线程可以看到的引用才是有害的。决定是否与其他对象共享this引用,需要你非常了解对象的可见性和这个对象会对这个引用干什么。

public class Safe { 

  private Object me;
  private Set set = new HashSet();
  private Thread thread;

  public Safe() { 
    // Safe because "me" is not visible from any other thread
    me = this;

    // Safe because "set" is not visible from any other thread
    set.add(this);

    // Safe because MyThread won't start until construction is complete
    // and the constructor doesn't publish the reference
    thread = new MyThread(this);
  }

  public void start() {
    thread.start();
  }

  private class MyThread(Object o) {
    private Object theObject;

    public MyThread(Object o) { 
      this.theObject = o;
    }

    ...
  }
}

public class Unsafe {
  public static Unsafe anInstance;
  public static Set set = new HashSet();
  private Set mySet = new HashSet();

  public Unsafe() {
    // Unsafe because anInstance is globally visible
    anInstance = this;

    // Unsafe because SomeOtherClass.anInstance is globally visible
    SomeOtherClass.anInstance = this;

    // Unsafe because SomeOtherClass might save the "this" reference
    // where another thread could see it
    SomeOtherClass.registerObject(this);

    // Unsafe because set is globally visible 
    set.add(this);

    // Unsafe because we are publishing a reference to mySet
    mySet.add(this);
    SomeOtherClass.someMethod(mySet);

    // Unsafe because the "this" object will be visible from the new
    // thread before the constructor completes
    thread = new MyThread(this);
    thread.start();
  }

  public Unsafe(Collection c) {
    // Unsafe because "c" may be visible from other threads
    c.add(this);
  }
}

正如上面看到的,安全的构造函数和非安全的构造函数和安全的构造函数很多地方很像,决定一个this是否对别的线程是否可见是非常困难的。最好的办法是在构造函数中不使用this引用(包含显示或隐式)。在构造器中创建非static的、内部类的对象,一定要注意this的使用。

更多不要this逃逸的理由

上面的例子,让我们看到了同步的必要性。当线程A启动线程B的时候,JLS保证线程A所有可见变量对线程B都可见。这是JLS提供的一种隐式的同步策略。如果我们在构造函数中启动线程,那么当前构建对象都没有完成,那么B看见的都是不完整的。

更多的 this 逃逸

public class Test
{
    private static Test lastCreatedInstance;

    public Test()
    {
      // .... 很多初始化工作
        lastCreatedInstance = this;
    }
}

如果lastCreatedInstance 前面有很多的初始化工作,但是另一个线程在初始化完成的时候使用了lastCreatedInstance,由于重排序(完全有可能)

另一个隐式this逃逸的例子

import java.util.*;

public class ThisEscape {
  private final int num;

  public ThisEscape(EventSource source) {
    source.registerListener(
        new EventListener() {
          public void onEvent(Event e) {
            doSomething(e);
          }
        });
    num = 42;
  }

  private void doSomething(Event e) {
    if (num != 42) {
      System.out.println("Race condition detected at " +
          new Date());
    }
  }
}    

public class Event { }
public interface EventListener {
  public void onEvent(Event e);
}
    

对于EventSource是比较复杂的,在我们的例子EventSource是一个Thread,用来不断的发送事件给最新的监听器。因为我们是产生race condition,所以我们只是发送一个事件给listenner。

import java.util.concurrent.*;

public class EventSource extends Thread {
  private final BlockingQueue<EventListener> listeners =
      new LinkedBlockingQueue<EventListener>();

  public void run() {
    while (true) {
      try {
        listeners.take().onEvent(null);
      } catch (InterruptedException e) {
        break;
      }
    }
  }

  public void registerListener(EventListener eventListener) {
    listeners.add(eventListener);
  }
}

开始测试

public class ThisEscapeTest {
  public static void main(String[] args) {
    EventSource es = new EventSource();
    es.start();
    while(true) {
      new ThisEscape(es);
    }
  }
}
  

如果打印出来为不是42,就发生了race condition。 其实举这些例子,是自己不知道如何将this泄漏给其他的线程。

构建线程安全构造器总结

让一个不完整的对象被其他线程看到,显然我们是不愿意看到的。然而,如果将this(隐式或者显示)发布出去,那么就会导致不完整的对象构造,不是说发布了this就一定会导致现在安全的问题,而是best practice 就是尽量不要在构造函数中将this暴露出去,如果暴露了,就应该避免其他的线程能够看到。

如何正确的发布一个对象

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

  • 引用本身要被其他线程看到
  • 对象的状态要被其他线程看到

在多线程编程中,首要的原则,就是要避免对象的共享,因为如果没有对象的共享,那么多线程编写要轻松得多,但是,如果要共享对象,那么除了能够正确的将构造函数书写正确外,如何正确的发布也是一个很重要的问题。发布的概念见上面。

下面的代码:

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

// Thread 2
hold.assertSanity(); //

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

  • 别的线程对于holder字段,可能会看到过时的值,这样就会导致空引用,或者是过时的值(即使holder已经被设置了)(引用本身没有被别的线程看到)
  • 更可怕的是,对于已经更新holder,及时能够看到引用的更新,但是对于对象的状态,看到的却可能是旧值,对于上面的代码,可能会抛出AssertionError异常。具体分析如下;

比如在上面 holder = new Holder(42),这句代码中,分开理想的情况如下:

Alloc Memory to pointer1
Write 42 to pointer1 at offset 0
Write pointer1 to someStaticVariable

但是由于Java是一个弱一致模型,上面的步骤,可能分成这样:

Alloc Memory to pointer1
Write pointer1 to someStaticVariable
Write 42 to pointer1 at offset 0

上面发布的时候,由于没有同步的操作,这样照成Thread2在将将holder设置成42时,先调用assertSanity(),这是看到的n是一个垃圾值,然后在获取右边n的值时,如果42被设置,那么就可能抛出异常。总结如下:

  • 即使通过new 来发布,那么引用的值别的线程也不一定看到。
  • 即使别的线程能够看到引用的新值,然而new创建对象中的普通的成员变量(没有用final修饰),可能也看不到。(也就是说通过构造函数设置了普通成员变量的值,但是构造函数返回了,由于重排序导致别的线程看到的是以前的垃圾值)。

安全发布的常用模式

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

  • 在静态初始化函数中初始化一个对象引用
  • 将一个对象引用保存在volatile类型的域或者是AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型的域中。
  • 将对象的引用保存到一个由锁保护的域中

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

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全将它发布给任何从这些容器中访问它的线程。
  • 通过将某个元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,可以将该元素安全的发布到任何从这些容器中访问该元素的线程。
  • 通过将元素放到BlockingQueue或者是ConcrrentLinkedQueue中,可以将该元素安全的发布到任何从这些访问队列中访问该元素的线程。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态初始化器:

public static Holder =  new Holder(42);

静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在同步机制,所以这种方式初始化对象都可以被安全的发布。对于可变对象,安全的发布之时确保在发布当时状态的可见性,而在随后的每次对象的访问时,同样需要使用同步来确保修改操作的可见性。

posted @ 2016-12-26 20:10  BruceChen7  阅读(1003)  评论(0编辑  收藏  举报