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內部存在同步機制,所以這種方式初始化對象都可以被安全的發布。對於可變對象,安全的發布之時確保在發布當時狀態的可見性,而在隨后的每次對象的訪問時,同樣需要使用同步來確保修改操作的可見性。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com