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