Java并發(fā)編程之并發(fā)代碼設計
引子
之前的文章我們探討了引發(fā)線程安全的原因主要是由于多線程的對共享內(nèi)存的操作導致的可見性或有序性被破壞,從而導致內(nèi)存一致性的錯誤。
那么如何設計并發(fā)代碼解決這個問題吶?
我們一般使用這幾種方式:
- 線程封閉
- 不可變對象
- 同步
發(fā)布和逸出
在此之前 我們先來了解一下發(fā)布和逸出的概念。
發(fā)布是指讓對象在當前作用域之外使用,例如將對象的引用傳遞到其他類的方法,在一個方法中返回其引用等。
在許多情況下我們要保證內(nèi)部對象不被發(fā)布,發(fā)布一些內(nèi)部狀態(tài)可能會破壞封裝性,讓使用者可以隨意改變其狀態(tài),從而破壞線程安全。
而在某些情況下,我們又需要發(fā)布某些內(nèi)部對象,如果需要線程安全的情況下,則需要正確的同步。
當一個對象在不應該被發(fā)布的時候發(fā)布了,這種情況就叫逸出。
- public class Escape {
- private List<User> users = Lists.newArrayList();
- public List<User> getUsers() {
- return users;
- }
- public void setUsers(List<User> users) {
- this.users = users;
- }
- }
getUsers已經(jīng)逸出了它的作用域,這個私有變量被發(fā)布了,因為任何調(diào)用者都可能修改數(shù)組。
同時發(fā)布users的時候也間接發(fā)布了User對象的引用。
- public class OuterEscape {
- private String str = "Outer's string";
- public class Inner {
- public void write() {
- System.out.println(OuterEscape.this.str);
- }
- }
- public static void main(String[] args) {
- OuterEscape out = new OuterEscape();
- OuterEscape.Inner in = out.new Inner();
- in.write();
- }
- }
在內(nèi)部類中保存了一個指向創(chuàng)建該內(nèi)部類的外圍類的引用,所以內(nèi)部類中可以使用創(chuàng)建該內(nèi)部類的外圍類的私有屬性、方法。
- public class ConstructorEscape {
- private Thread t;
- public ConstructorEscape() {
- System.out.println(this);
- t = new Thread() {
- public void run() {
- System.out.println(ConstructorEscape.this);
- }
- };
- t.start();
- }
- public static void main(String[] args) {
- ConstructorEscape a = new ConstructorEscape();
- }
- }
this引用被線程t共享,故線程t的發(fā)布將導致ConstructorEscape對象的發(fā)布,由于ConstructorEscape對象被發(fā)布時還未構(gòu)造完成,這將導致ConstructorEscape對象逸出
總結(jié)一下如何安全發(fā)布的步驟
- 找出構(gòu)成對象狀態(tài)的所有變量
- 找出約束狀態(tài)變量的不變性條件
- 建立對象狀態(tài)的并發(fā)訪問策略
線程封閉
線程封閉的思想很簡單,既然線程安全問題是由于多線程對共享變量的訪問造成的,那么如果我們可以避免操作共享變量,每個線程訪問自己的變量,就不會有線程安全的問題,這是實現(xiàn)線程安全最簡單的方法。
通過線程控制逃逸規(guī)則可以幫助你判斷代碼中對某些資源的訪問是否是線程安全的,如果一個資源的創(chuàng)建,使用,銷毀都在同一個線程內(nèi)完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的。
資源可以是對象,數(shù)組,文件,數(shù)據(jù)庫連接,套接字等等。Java中你無需主動銷毀對象,所以“銷毀”指不再有引用指向?qū)ο?。即使對象本身線程安全,但如果該對象中包含其他資源(文件,數(shù)據(jù)庫連接),整個應用也許就不再是線程安全的了。比如2個線程都創(chuàng)建了各自的數(shù)據(jù)庫連接,每個連接自身是線程安全的,但它們所連接到的同一個數(shù)據(jù)庫也許不是線程安全的
我們再來看線程封閉的幾種實現(xiàn)方式:
棧封閉
棧封閉是線程封閉的一個特例,在棧封閉中只能通過局部變量來訪問對象,局部變量存儲在線程自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。
對象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲在線程的棧內(nèi)。所有的對象都存在共享堆中。如果在某個方法中創(chuàng)建的對象不會逸出該方法,那么它就是線程安全的。實際上,哪怕將這個對象作為參數(shù)傳給其它方法,只要別的線程獲取不到這個對象,那它仍是線程安全的。
- public void someMethod(){
- LocalObject localObject = new LocalObject();
- localObject.callMethod();
- method2(localObject);
- }
- public void method2(LocalObject localObject){
- localObject.setValue("value");
- }
如上,LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每個執(zhí)行someMethod()的線程都會創(chuàng)建自己的LocalObject對象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實上,整個someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。當然,如果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了
程序控制線程封閉
通過程序?qū)崿F(xiàn)來進行線程封閉,也就是說我們無法利用語言特性將對象封閉到特定的線程上,這一點導致這種方式顯得不那么可靠假設我們保證只有一個線程可以對某個共享的對象進行寫入操作,那么這個對象的"讀取-修改-寫入"在任何情況下都不會出現(xiàn)竟態(tài)條件。如果我們?yōu)檫@個對象加上volatile修飾則可以保證該對象的可見性,任何線程都可以讀取該對象,但只有一個線程可以對其進行寫入。這樣,僅僅通過volatile修飾就適當?shù)乇WC了其安全性,相比直接使用synchoronized修飾,雖然更適合,但實現(xiàn)起來稍微復雜。
程序控制線程封閉,這個不是一種具體的技術(shù),而是一種設計思路,從設計上把處理一個對象狀態(tài)的代碼都放到一個線程中去,從而避免線程安全的問題。
ThreadLocal
ThreadLocal機制本質(zhì)上是程序控制線程封閉,只不過是Java本身幫忙處理了 。來看Java的Thread類和ThreadLocal類:
- Thread線程類維護了一個ThreadLocalMap的實例變量
- ThreadLocalMap就是一個Map結(jié)構(gòu)
- ThreadLocal的set方法取到當前線程,拿到當前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,把要放入的值作為value,放到Map
- ThreadLocal的get方法取到當前線程,拿到當前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,拿到對應的value
- public class Thread implements Runnable {
- ThreadLocal.ThreadLocalMap threadLocals = null;
- }
- public class ThreadLocal<T> {
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null)
- return (T)e.value;
- }
- return setInitialValue();
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- }
ThreadLocal的設計很簡單,就是給線程對象設置了一個內(nèi)部的Map,可以放置一些數(shù)據(jù)。JVM從底層保證了Thread對象之間不會看到對方的數(shù)據(jù)。
使用ThreadLocal前提是給每個ThreadLocal保存一個單獨的對象,這個對象不能是在多個ThreadLocal共享的,否則這個對象也是線程不安全的
ThreadLocal 內(nèi)存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統(tǒng) GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內(nèi)存泄漏。
其實,ThreadLocalMap的設計中已經(jīng)考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
所以每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)就可以避免這個問題
不可變對象
一個對象如果在創(chuàng)建后不能被修改,那么就稱為不可變對象。在并發(fā)編程中,一種被普遍認可的原則就是:盡可能的使用不可變對象來創(chuàng)建簡單、可靠的代碼
在并發(fā)編程中,不可變對象特別有用。由于創(chuàng)建后不能被修改,所以不會出現(xiàn)操作共享變量導致的內(nèi)存一致性錯誤
但是程序員們通常并不熱衷于使用不可變對象,因為他們擔心每次創(chuàng)建新對象的開銷。實際上這種開銷常常被過分高估,而且使用不可變對象所帶來的一些效率提升也抵消了這種開銷
我們先來看一個使用同步來解決線程安全的例子
- public class SynchronizedRGB {
- // Values must be between 0 and 255.
- private int red;
- private int green;
- private int blue;
- private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public SynchronizedRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public void set(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- synchronized (this) {
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- }
- public synchronized int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public synchronized String getName() {
- return name;
- }
- }
- SynchronizedRGB color =
- new SynchronizedRGB(0, 0, 0, "Pitch Black");
- ...
- int myColorInt = color.getRGB(); // 1
- String myColorName = color.getName(); // 2
- //如果其他線程在1執(zhí)行后調(diào)用set方法 就會導致 getName 跟getRGB的值不匹配
- synchronized (color) {
- int myColorInt = color.getRGB();
- String myColorName = color.getName();
- }
- //必需使這2個語句同步執(zhí)行
創(chuàng)建不可變對象的幾條原則
- 不提供修改可變對象的方法。(包括修改字段的方法和修改字段引用對象的方法)
- 將類的所有字段定義為final、private的。
- 不允許子類重寫方法。簡單的辦法是將類聲明為final,更好的方法是將構(gòu)造函數(shù)聲明為私有的,通過工廠方法創(chuàng)建對象。
- 如果類的字段是對可變對象的引用,不允許修改被引用對象。
- 不共享可變對象的引用。當一個引用被當做參數(shù)傳遞給構(gòu)造函數(shù),而這個引用指向的是一個外部的可變對象時,一定不要保存這個引用。如果必須要保存,那么創(chuàng)建可變對象的拷貝,然后保存拷貝對象的引用。同樣如果需要返回內(nèi)部的可變對象時,不要返回可變對象本身,而是返回其拷貝
修改后的例子
- final public class ImmutableRGB {
- // Values must be between 0 and 255.
- final private int red;
- final private int green;
- final private int blue;
- final private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public ImmutableRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public String getName() {
- return name;
- }
- }
事實不可變對象
如果對象本事是可變的,但是程序運行過程中,不存在改變的可能,那么就稱為事實不可變對象,這樣也不需要額外的線程安全的保護
同步
當我們不得不使用共享變量,而且需要經(jīng)常修改的時候我們就需要使用同步來實現(xiàn)線程安全了。
Java我們可以使用 Synchronized/Lock volatite CAS 來實現(xiàn)同步。
synchronized是一種獨占鎖,它假設最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執(zhí)行,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
與鎖相比,volatile變量是一和更輕量級的同步機制,因為在使用這些變量時不會發(fā)生上下文切換和線程調(diào)度等操作,但是volatile變量也存在一些局限:不能用于構(gòu)建原子的復合操作,因此當一個變量依賴舊值時就不能使用volatile變量。
CAS是一種樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
同步解決了三個相互關(guān)聯(lián)的問題:
- 原子性:哪些指令必須是不可分割的
- 可見性:一個線程執(zhí)行的結(jié)果對另一個線程是可見的
- 有序性:某個線程的操作結(jié)果對其它線程來看是無序的
總結(jié)
理解線程安全的概念很重要, 所謂線程安全問題,就是處理對象狀態(tài)的問題 。如果要處理的對象是無狀態(tài)的(不變性),或者可以避免多個線程共享的(線程封閉),那么我們可以放心,這個對象可能是線程安全的。當無法避免,必須要共享這個對象狀態(tài)給多線程訪問時,這時候才用到線程同步的一系列技術(shù)。
這個理解放大到架構(gòu)層面,我們來設計業(yè)務層代碼時,業(yè)務層***做到無狀態(tài),這樣就業(yè)務層就具備了可伸縮性,可以通過橫向擴展平滑應對高并發(fā)。
所以我們處理線程安全可以有幾個層次:
- 能否做成無狀態(tài)的不變對象。無狀態(tài)是最安全的。
- 能否線程封閉
- 采用何種同步技術(shù) (Synchronized/Lock volatite CAS)