一、對象的發布和逸出
發布(publish)對象意味着其作用域之外的代碼可以訪問操作此對象。例如將對象的引用保存到其他代碼可以訪問的地方,或者在非私有的方法中返回對象的引用,或者將對象的引用傳遞給其他類的方法。
為了保證對象的線程安全性,很多時候我們要避免發布對象,但是有時候我們又需要使用同步來安全的發布某些對象。
逸出即為發布了本不該發布的對象。
使用靜態變量引用對象是發布對象最直觀和最簡單的方式。例如以下代碼示例,在示例中我們也看到,由於任何代碼都可以遍歷我們發布persons集合,導致我們間接的發布了Person實例,自然也就造成可以肆意的訪問操作集合中的Person元素。
package com.codeartist; import java.util.HashSet; public class ObjectPublish { public static HashSet<Person> persons ; public void init() { persons = new HashSet<Person>(); } }
在非私有的方法內返回一個私有變量的引用會導致私有變量的逸出,例如以下代碼
package com.codeartist; import java.util.HashSet; public class ObjectPublish { private HashSet<Person> persons= new HashSet<Person>(); public HashSet<Person> getPersons() { return this.persons; } }
發布一個對象也會導致此對象的所有非私有的字段對象的發布,其中也包括方法調用返回的對象。
在構造函數中使用直接初始化或者調用可改寫的實例方法都會導致隱式的this逸出也是經常發生的事情,例如以下代碼,在EventListener的實例中也通過this隱含的發布了尚未構造完成的ConstructorEscape實例,可能會造成無法預知的結果。
package com.codeartist; public class ConstructorEscape { public ConstructorEscape(EventSource eventSource) { eventSource.registerListener( new EventListener(){ public void OnEvent(Event e) { doSomeThing(e); } } ); } }
我們可以使用工廠方法防止隱式的this逸出問題,例如以下代碼
package com.codeartist; public class ConstructorEscape { private final EventListener listener; private ConstructorEscape() { this.listener= new EventListener(){ public void OnEvent(Event e) { doSomeThing(e); } }; } public static ConstructorEscape getInstance(EventSource eventSource) { ConstructorEscape instance = new ConstructorEscape(); eventSource.registerListener(instance.listener); return instance; } }
二、避免對象發布之線程封閉
線程封閉可以使數據的訪問限制在單個線程之內,相對鎖定同步來說,其實實現線程安全比較簡單的方式。
java提供了ThreadLocal類來實現線程封閉,其可以使針對每個線程存有共享狀態的獨立副本。其通常用於防止對可變的單實例變量和全局變量進行共享,例如每個請求作為一個邏輯事務需要初始化自己的事務上下文,這個事務上下文應該使用ThreadLocal來實現線程封閉。
棧封閉是線程封閉的特例,即數據作為局部變量封閉在執行線程中,對於值類型的局部變量不存在逸出的問題,如果是引用類型的局部變量,開發人員需要確保其不要作為返回值或者其他的關聯引用等而被逸出。
三、避免對象發布之不變性
某個對象創建之后就不能修改其狀態,那么我們就說這個對象是不可變對象。
由於多線程操作可變狀態會導致原子性、可見性一系列問題,所以線程安全性是不可變對象與生俱來的特性。
不可變對象由構造函數初始化狀態,並可以安全的傳遞給任何不可信代碼使用。
所有字段標記為final的對象,由於引用字段的對象可能可以直接修改,所以其並不一定是不可變對象,其需要滿足以下條件
對象的所有字段都用final標記
對象創建之后任何狀態都不能修改
對象不存在this隱式構造函數逸出
package com.codeartist; import java.util.HashSet; public class ObjectPublish { private HashSet<Person> persons= new HashSet<Person>(); public ObjectPublish() { persons.add(new Person("wufengtinghai")); persons.add(new Person("codeartist")); } public Person getPerson(String name) { Person result = null; for(Person p : this.persons) { if(p.name == name) { result = p.Clone(); } } return result; } }
四、對象的安全發布
很多時候我們是希望在多線程之間共享數據的,此時我們就必須確保安全的發布共享對象。
要安全的發布一個對象,對象的引用以及對象的狀態對其他線程都是可見的,一個正確構造的對象可以通過以下方式安全的發布
在靜態構造函數中初始化對象引用
使用volatile和AtomicReferance限定對象引用
使用final限定對象引用
將對象引用保存到有鎖保護的字段中
1.不可變對象
任何線程都可以在不需要同步的情況下安全的訪問不可變對象及其final字段,如果字段的引用可以改變則需要進行同步。
不可變對象在確保初始化安全的前提,可以自由的發布
有時為了確保不可變對象對於多個線程呈現一致的狀態,需要使用同步不可變對象的初始化。
需要具備以上說的三個不變性限制條件。
2.隱式約定不可變對象
實際可以改變對象在發布后不會再改變狀態,則此對象成為隱式約定不可變對象。
雖然任何線程都可以無需同步即可安全的訪問隱式約定不可變對象,但是由於其本身還是可變的,所以其需要以安全方式進行發布。
3.可變對象
需要使用同步來確保發布和共享的安全性。
五、對象的安全共享
為了確保使用共享對象的安全性,我們需要遵循其既定的規則(例如是否是不可變對象)來確定我們訪問對象的方式
不可變對象和隱式約定不可變對象可以直接由多線程並發訪問。
線程封閉對象只能由創建線程持有並修改。
自己內部實現安全性的線程安全對象也可以直接由多線程訪問。
一般的可變對象只能通過持有鎖進行同步來實現安全共享。