Concurrency Item - 線程封閉

保證併發安全性的方式有三:

·不共享

·不可變

·同步



前兩種方式相對第三種要簡單一些。

這一篇不說語言特性和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。

而這種做法會導致全局變量難以抽象,並降低其可重用性,而且也增加了耦合。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章