java 共享软件 保护_【Java并发.3】对象的共享
本章將介紹如何共享和發(fā)布對象,從而使他們能夠安全地由多個線程同時訪問。這兩章合在一起就形成了構(gòu)建線程安全類以及通過java.util.concurrent 類庫來構(gòu)建開發(fā)并發(fā)應(yīng)用程序的重要基礎(chǔ)。
3.1 可見性
可見性是一種復(fù)雜的屬性,因為可見性中的錯誤總是違背我們的直覺。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機制。
在下面的清單中 NoVisibility 說明了當多個線程在沒有同步的情況下共享數(shù)據(jù)出現(xiàn)的錯誤。主線程啟動讀線程,然后將 number 設(shè)為 42,并將 ready 設(shè)為 true。讀線程一直循環(huán)直到發(fā)現(xiàn) ready 的值變?yōu)?true,然后輸出 number 的值。雖然看起來會輸出 42,但事實上可能輸出 0,或者根本無法終止。這是因為代碼中沒有使用足夠的同步機制,因此無法保證主線程寫入的ready 值和 nunber 值對于讀線程來說是可見的。
public classNoVisibility { 【皺眉臉-不要這樣做】private static booleanready;private static intnumber;public static voidmain(String[] args) {newReaderThread().start();
number= 42;
ready= true;
}private static class ReaderThread extendsThread {public voidrun() {while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
}
NoVisibility 可能會持續(xù)循環(huán)下去,因為讀線程可能永遠都看不到 ready 值。一種更奇怪的現(xiàn)象是,NoVisibility 可能會輸出 0,因為讀線程可能看到了寫入 ready 值,但卻沒有看到之前寫入 number 值,這種現(xiàn)象稱為“重排序(Reordering)”。(注釋:這看上去似乎是一種失敗的設(shè)計,但卻是使 JVM 充分地利用現(xiàn)代多核處理器的強大性能。)
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執(zhí)行順序進行一些意想不到的調(diào)整。在缺乏足夠同步的多線程程序中,要想對內(nèi)存操作的執(zhí)行順序進行判斷,幾乎無法得出正確的結(jié)論。
3.1.1 失效數(shù)據(jù)
NoVisibility 展示了在缺乏同步的程序中可能產(chǎn)生錯誤結(jié)果的一種情況:失效數(shù)據(jù)。當讀線程查看 ready 變量時,可能會得到一個已經(jīng)失效的值。除非在每次訪問變量時都使用同步,否則很可能獲得該變量的一個失效值。更糟糕的是,失效值可能不會同時出現(xiàn):一個程序可能獲得某個變量的最新值,而獲得另一個變量的失效值。
失效數(shù)據(jù)還可能導(dǎo)致一些令人困惑的故障,例如意料之外的異常、被破壞的數(shù)據(jù)結(jié)構(gòu)、不精確的計算以及無限循環(huán)等。
在如下程序清單 Mutableinteger 不是線程安全的,因為 get 和 set 都是沒有同步的情況下訪問 value 的。如果某個線程調(diào)用了 set,那么另一個在調(diào)用的get 線程可能會看到更新后的值,也可能看不到。
public classMutableInteger {private intvalue;public intget() {returnvalue;
}public void set(intvalue) {this.value =value;
}
}
在程序清代 SynchronizedInteger 中,通過對 get 和 set 方法進行同步,可以使MutableInteger 成為一個線程安全的類。僅對 set 方法進行同步時不夠的,調(diào)用 get 線程仍然會看到失效值。
public classSynchronizedInteger {private intvalue;public synchronizedintget() {returnvalue;
}public synchronizedvoid set(intvalue) {this.value =value;
}
}
3.1.2 非原子的64位操作
忽略。。。
3.1.3 加鎖與可見性
內(nèi)置鎖可以用于確保某個線程以一種可預(yù)測的方式來查看另一個線程的執(zhí)行結(jié)果。對于同一個鎖,后面進入鎖的線程可以看到之前線程在鎖中的所有操作結(jié)果(加鎖可以保證可見性)。
加鎖的含義不僅僅局限于互斥行為,還包括內(nèi)存可見性。為了確保所有線程都能看到共享變量的最新值,所有執(zhí)行讀操作或者寫操作的線程都必須在同一個鎖上同步
3.1.4 Volatile變量
對于volatile 關(guān)鍵字的詳細介紹,建議大家去仔細觀看 volatile關(guān)鍵字解析?,所以在這不做介紹。
3.2 發(fā)布與逸出
“發(fā)布(Publish)”一個對象的一起是指,是對象能夠在當前作用域之外的代碼中使用。例如,將一個指向該對象的引用保存到其他代碼可以訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其他類的方法中。在許多情況中,我們要確保對象及其內(nèi)部狀態(tài)不被發(fā)布。而在某些情況下,我們又需要發(fā)布某個對象,但如果在發(fā)布時要確保線程安全性,則可能需要同步。當某個不應(yīng)該發(fā)布的對象被發(fā)布時,這種情況就被稱為逸出(Escape)。
發(fā)布對象最簡單的方法就是將對象的引用保存到一個公有的靜態(tài)變量中,以便任何類和線程都能看見該對象,如下。發(fā)布一個對象
public classKnownSecrets {public static SetknownSecrets;public voidinitialize() {
knownSecrets= new HashSet();
}
}
程序清單:是內(nèi)部的可變狀態(tài)逸出:
public classUnsafeStates {private String[] states = new String[] {"AK","AL"...};publicString[] getStates() {returnstates;
}
}
如何按照上述方式來發(fā)布 states,就會出現(xiàn)問題,因為任何調(diào)用者都能修改這個數(shù)組的內(nèi)容。在這個實例中,數(shù)組 states 已經(jīng)逸出了它所在的作用域,因為這個本應(yīng)是私有的變量已經(jīng)被發(fā)布了。
當發(fā)布一個對象時,在該對象的非私有域中引用的所有對象同樣會被發(fā)布。一般來說,如果一個已經(jīng)發(fā)布的對象能夠通過非私有的變量引用和方法調(diào)用到達其他的對象,那么這些對象也都會被發(fā)布。
3.3 線程封閉
當訪問共享的可變數(shù)據(jù)時,通常需要使用同步。一種避免使用同步的方式就是不同享數(shù)據(jù)。如果僅在單線程內(nèi)訪問數(shù)據(jù),就不需要同步。這種技術(shù)稱為線程封閉(Thread Confinement),它是實現(xiàn)線程安全性的最簡單方式之一。
線程封閉技術(shù)的常見應(yīng)用時 JDBC 的 Connection 對象。線程從連接池中獲得一個 Connection 對象,并且用該對象來處理請求,使用完后再將對象返還給連接池。由于大多數(shù)請求都是由單個線程采用同步的方式來處理,并且在 Connection 對象返回之前,連接池不會再將它分配給其他線程,因此,這種連接管理模式在處理請求時隱含地將 Connection 對象封閉在線程中。
3.3.1 Ad-hoc線程封閉
略...
3.3.2 棧封閉
棧封閉式線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象。局部變量的固有屬性之一就是封閉在執(zhí)行線程中。它們位于執(zhí)行線程的棧中,其他線程無法訪問這個棧。棧封閉(也被稱為線程內(nèi)部使用或者線程局部使用,不要與核心類庫中的 ThreadLocal 混淆)。
對于基本類型的局部變量,如下程序清單中 loadTheArk 方法的 numPairs,無論如何都不會破壞棧封閉性,由于任何方法都無法獲得基本類型的引用,因此Java 語言的這種語義就確保了基本來興的局部變量始終封閉在線程內(nèi)。
public int loadTheArk(Collectioncandidates) {
SortedSetanimals;int numPairs = 0; //基本類型的局部變量
Aniaml candidate = null;//animals 被封閉在方法中,不要使它們逸出
animals = new TreeSet(newSpeciesGenderComparator());
animals.addAll(candidates);for(Animal a : animals) {
numPairs++;
}returnnumPairs;
}
3.3.3 ThreadLocal 類
維持線程封閉性的一種更規(guī)范方法就是使用 ThreadLocal,這個類能使線程中的某個值與保存值的對象關(guān)聯(lián)起來。ThreadLocal 提供了 get 和 set 等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此 get 總是返回由當前執(zhí)行線程在調(diào)用 set 時設(shè)置的最新值。
ThreadLocal 對象通常用于放置對可變的單實例變量(Singleton)或全局變量進行共享。例如,在單線程應(yīng)用程序中可能會維持一個全局的數(shù)據(jù)庫連接,并在程序啟動時初始化這個連接對象,從而避免在調(diào)用每個方法時都要傳遞一個 Connection 對象。
private static ThreadLocal connectionThreadLocal = new ThreadLocal<>() {
@OverrideprotectedObject initialValue() {returnDriverManager.getConnection(URL);
}
}public staticConnection getConnection() {returnconnectionThreadLocal.get();
}
當某個線程初次調(diào)用 ThreadLocal.get 方法時,就會調(diào)用 initialValue 來獲取初始值。從概念上看,你可以將ThreadLocal 視為包含了 Map 對象,其中保存了特定于該線程的值,但 ThreadLocal 的實現(xiàn)并非如此。這些特定于線程的值保存在 Thread 對象,當線程終止后,這些值會作為垃圾回收。
3.4 不變性
滿足同步需求的另一種方法時使用不可變對象。到目前為止,我們介紹了許多與原子性和可見性相關(guān)的問題,例如得到失效數(shù)據(jù),丟失更新操作或者觀察到某個對象處于不一致的狀態(tài)等等,都與多線程試圖同時訪問同一個可變的狀態(tài)相關(guān)。如果對象的狀態(tài)不會改變,那么這些問題與復(fù)雜性也就自然消失了。
不可變對象一定是線程安全的。
雖然在Java 語言規(guī)范和 Java 內(nèi)存模型中都沒有給出不可變性的正式定義,但不可變性并不等于將對象中所有的域都聲明為 final 類型,即使對象中所有的域都是 final 類型的,這個對象也仍然是可變的,因為在 final 類型的域中可以保存對可變對象的引用。
當滿足以下條件時,對象才是不可變的:
對象創(chuàng)建以后其狀態(tài)不可能修改。
對象的所有域都是 final 類型。
對象時正確創(chuàng)建的(在對象的創(chuàng)建期間, this 引用沒有逸出)。
看個例子:在可變對象基礎(chǔ)上構(gòu)建的不可變類
public classThreeStooges {private final Set stooges = new HashSet<>();publicThreeStooges() {
stooges.add("one");
stooges.add("two");
stooges.add("three");
}public booleanisStooge(String name) {returnstooges.contains(name);
}
}
3.4.1 Final 域
在 Java 內(nèi)存模型中,final 域還有著特殊的語義。final 域能確保初始化過程的安全性,從而可以不受限制地訪問不可變對象,并在共享這些對象時無需同步。
正如“除非需要更高的可見性,否則應(yīng)將所有的域都聲明為私有域”是一個良好的編程習(xí)慣,“除非需要某個域是可變的,否則應(yīng)將其聲明為 final 域”也是一個良好的編程習(xí)慣。
3.4.2 示例:使用 volatile 類型來發(fā)布不可變對象
對于volatile 關(guān)鍵字的詳細介紹,建議大家去仔細觀看?volatile關(guān)鍵字解析?,所以在這不做過多介紹。貼一個代碼:
public class VolatileCachedFactorizer implementsServlet {private volatile OneValueCache cache = new OneValueCache(null, null);public voidservice(ServletRequest request, ServletResponse response) {
BigInteger i=extractFromRequest(request);
BigInteger[] factors=cache.getFactors(i);if (factors == null) {
factors=factor(i);
cache= newOneValueCache(i, factors);
}
encodeIntoResponse(response, factors);
}
}
3.5 安全發(fā)布
到目前為止,我么重點討論的是如何確保對象不被發(fā)布,例如讓對象封閉在線程或另一個對象的內(nèi)部。當然,在某些情況下我們希望多個線程間共享對象,此時必須確保安全地進行共享。
如下:在沒有足夠同步的情況下發(fā)布對象(不要這樣做)
//不安全的發(fā)布
publicHolder holder;public voidinitialize() {
holder= new Holder(42);
}
由于可見性問題,其他線程看到的 Holder 對象將處于不一致的狀態(tài),即便在該對象的構(gòu)建函數(shù)中已經(jīng)正確地構(gòu)建了不便性條件。這種不正確的發(fā)布導(dǎo)致其他線程看到尚未創(chuàng)建完成的對象。
3.5.1 不正確的發(fā)布:正確的對象被破壞
你不能指望一個尚未被完全創(chuàng)建的對象擁有完整性。某個觀察該對象的線程將看到對象處于不一致的狀態(tài),然后看到對象的狀態(tài)突然發(fā)生變化,即使線程在對象發(fā)布后還沒有修改過它。
如下:由于未被正確發(fā)布,因此這個類可能出現(xiàn)故障
public classHolder {private intn;public Holder(intn) {this.n =n;
}public voidassertSanity() {if(n != n) //這句沒看懂,就算同步時會出現(xiàn) n 很可能成為失效值,但是難道 (n != n)不是原子操作?求解。
throw new AssertionError("this statement is false");
}
}
3.5.2 不可變對象與初始化安全性
由于不可變對象是一種非常重要的對象,因此Java 內(nèi)存模型為不可變對象的共享提供了一種特殊的初始化安全性保障。
任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發(fā)布這些對象時沒有使用同步。
3.5.3 安全發(fā)布的常用模式
要安全地發(fā)布一個對象,對象的引用以及對象的狀態(tài)必須同時對其他線程可見。一個正確構(gòu)造的對象可以通過以下方式來安全地發(fā)布:
在靜態(tài)初始化函數(shù)中初始化一個對象引用。
將對象的引用保存到 volatile 類型的域或者 AtomicReferance 對象中
將對象的引用保存到某個正確構(gòu)造對象的 final 類型域中。
將對象的引用保存到一個由鎖保護的域中。
線程安全庫中的容器類提供了一下的安全發(fā)布保證:
通過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,可以安全地將它發(fā)布給任何從這些同期中訪問它的線程(無論是直接訪問還是通過迭代器訪問)
通過將某個元素放入Vector、CopyiOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet 中,可以將該元素安全地發(fā)布到任何從這些容器中訪問該元素的線程。
通過將某個元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,可以將該元素安全地發(fā)布到任何從這些隊列中訪問該元素的線程。
通常,要發(fā)布一個靜態(tài)構(gòu)造的對象,最簡單和最安全的方式是使用靜態(tài)的初始化器:
public static Holder holder = new Holder(42);
3.5.4 事實不可變對象
如果對象在發(fā)布后不會被修改,那么 程序只需將它們視為不可變對象即可。
在沒有額外的同步情況下,任何線程都可以安全地使用被安全發(fā)布的事實不可變對象。
例如,Date 本身是可變的,但如果將它作為不可變對象來使用,那么在多個線程之間共享 Date 對象時,就可以省去對鎖的使用。假設(shè)需要維護一個 Map 對象,其中保存了每位用戶的最近登錄時間:
public Map lastLogin = Collections.synchronizedMap(new HashMap());
如果Date對象的值在被放入Map 后就不會改變,那么 synchronizedMap 中的同步機制就足以使 Date 值被安全地發(fā)布,并且在訪問這些 Date 值時不需要額外的同步。
3.5.5 可變對象
對于可變對象,不僅在發(fā)布對象時需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保后續(xù)修改操作的可見性。
對象的發(fā)布需要取決于它的可變性:
不可變對象可以通過任何機制來發(fā)布
事實不可變對象必須通過安全方式來發(fā)布。
可變對象必須通過安全方式來發(fā)布,并且必須是線程安全的或者由某個鎖保護起來。
3.4.5 安全地共享對象
當發(fā)布一個對象時,必須明確地說明對象的訪問方式。
在并發(fā)程序中使用和共享對象時,可以使用一些實用的策略包括:
線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只能由這個線程修改。
只讀共享:在沒有額外同步的情況下,共享的只讀對象可以由多個線程并發(fā)訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
線程安全共享:線程安全的對象在其內(nèi)部實現(xiàn)同步,因此對個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
保護對象:被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發(fā)布的并且由某個特定鎖保護的對象。
總結(jié)
以上是生活随笔為你收集整理的java 共享软件 保护_【Java并发.3】对象的共享的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [云炬创业学笔记]第二章决定成为创业者测
- 下一篇: 华为申请注册华为鸿蒙商标,华为申请注册“