保證併發安全性的方式有三:
·不共享
·不可變
·同步
前兩種方式相對第三種要簡單一些。
這一篇不說語言特性和API提供的相關同步機制,主要記錄一下關於共享的一些思考。
共享(shared),可以簡單地認爲多個線程可以同時訪問某個對象。
如果僅僅在單線程內進行訪問則不存在同步的問題。
保證數據的單線程訪問稱爲線程封閉(thread confinement)。
線程封閉有三種方式:
·Ad-hoc線程封閉
·棧封閉
·ThreadLocal
Ad-hoc線程封閉:通過程序實現來進行線程封閉,也就是說我們無法利用語言特性將對象封閉到特定的線程上,這一點導致這種方式顯得不那麼可靠。
舉個例子,假設我們保證只有一個線程可以對某個共享的對象進行寫入操作,那麼這個對象的"讀取-修改-寫入"(比如自增操作)在任何情況下都不會出現竟態條件。
如果我們爲這個對象加上volatile修飾則可以保證該對象的可見性,任何線程都可以讀取該對象,但只有一個線程可以對其進行寫入。
這樣,僅僅通過線程封閉+volatile修飾就適當地保證了其安全性,相比直接使用synchoronized修飾,雖然更適合,但實現起來稍微複雜。
而對於線程封閉方式的選擇,這種方式是最不被推薦的。
棧封閉:這個方式理解起來比較簡單,封閉在執行線程是局部變量本身固有的特性,封閉在執行線程的棧裏,其他線程無法訪問是理所當然的。
對於基本類型的局部變量,我們不用考慮任何事情,因爲Java語言特性本身就保證了任何方法都無法獲得基本類型的引用。
而對於引用類型的局部變量,我們需要稍微注意一些問題來保證其棧封閉。
參考下面的裝載方舟的代碼,現在我們要保護animals,則需要保證該方法的參數、調用的外來方法、返回值都不會引用到animals:
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; }
先說說loadTheArk的參數candidates,我們將它的元素進行篩選後裝載到了方舟中,方法結束後無法通過該參數影響方舟中的動物夫婦。
其次是外來方法,我們使用了"種類性別比較器"對animals進行排序,但它是一個concrete,不會有不確定的行爲對animals的狀態產生影響。
最後是返回值,顯然我們是想報告裝載了多少對動物夫婦,返回類型是個基本類型,無法引用animals。
好了,這就是個成功的棧封閉。
ThreadLocal:給人一種親切感,這幾乎是很常見的方式,而且也是最規範的方式。
我們通常用ThreadLocal保證可變的單例變量和全局變量不被多線程共享。
先讓我們想想單線程場景中使用Connection對象連接數據庫,鑑於Connection對象的初始化開銷,整個應用中會維護一個全局的Connection對象。
如果我們想將這個應用改爲多線程的,鑑於Connection對象本身不是線程安全的,我們需要對其進行線程封閉,此時我們可以使用ThreadLocal:
public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); } }
不僅是Connection這種場景,如果我們的很多操作頻繁地用到某個對象,而我們又需要考慮它的線程封閉又需要考慮它的初始化開銷,ThreadLocal幾乎是最好的選擇。
雖然這看起來有點像一個全局的Map<Thread,T>,事實上也可以這樣理解,但其實現並不是這樣你懂的。
當然,這種方式很方便,但這並不代表ThreadLocal可以濫用, 比如僅僅是考慮到應用的併發安全性就把全局變量一律變成ThreadLocal。
而這種做法會導致全局變量難以抽象,並降低其可重用性,而且也增加了耦合。