Java并發(fā)編程之對象的共享
1.可見性
通常,我們無法保證執(zhí)行讀操作的線程能看到其他線程寫入的值,因為每個線程都由自己的緩存機制。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機制。
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
以上代碼,看起來會輸出42,但事實上很可能根本無法終止,因為讀線程永遠看不到ready的值;很有可能輸出0,因為讀線程看到了寫入ready的值,卻沒有看到之后寫入number的值,這種現(xiàn)象稱為“重排序”。在沒有同步的情況下,編譯器、處理器、運行時等都有可能對操作的執(zhí)行順序進行一些意想不到的調(diào)整。
所以,只要有數(shù)據(jù)在多個線程之間共享時,就應(yīng)該使用正確的同步。
1.1 失效數(shù)據(jù)
除非使用同步,否則很可能獲得變量的失效值。失效值可能不會同時出現(xiàn),一個線程可能獲得一個變量的最新值,而獲得另一個變量的失效值。失效數(shù)據(jù)還可能導(dǎo)致一些令人困惑的故障,如:意料之外的異常、被破壞的數(shù)據(jù)結(jié)構(gòu)、不精確的計算、無限循環(huán)等等。
1.2 非原子的64位操作
對于非volatile類型的long和double變量,JVM允許將64位的讀操作或?qū)懖僮鞣纸鉃閮蓚€32位的操作。所以,很可能會讀取到最新值的高32位和失效值的低32值,造成讀取到是一個隨機值。除非用關(guān)鍵字volatile來聲明它們,或者用鎖保護起來。
1.3 加鎖和可見性
當(dāng)某線程執(zhí)行由鎖保護的同步代碼塊時,可以看到其他線程之前在同一同步代碼塊中的所有操作結(jié)果。如果沒有同步,將無法實現(xiàn)上述保證。加鎖的含義不僅僅局限于互斥行為,還包括可見性。為了確保所有線程都能看到共享變量的最新值,所有執(zhí)行讀操作或?qū)懖僮鞯木€程都必須在同一個鎖上同步。
1.4 volatile變量
當(dāng)把變量聲明為volatile類型后,編譯器和運行時都不會將該變量上的操作也其他內(nèi)存操作一起重排序。volatile變量不會被緩存在寄存器或者其他處理器不可見的地方,因此在讀取volatile變量時總會返回最新寫入的值。加鎖機制既可以確??梢娦杂挚梢源_保原子性,而volatile
變量只能確保可見性。
當(dāng)且僅當(dāng)滿足以下所有條件時,才應(yīng)該使用volatile變量:
- 對變量的寫入操作不依賴變量的當(dāng)前值,或者能確保只用單個線程更新變量的值。
- 該變量不會與其他狀態(tài)變量一起納入不變性條件中。
- 在訪問變量時不需要加鎖。
2. 發(fā)布與泄露
發(fā)布一個對象是指,是對象能夠在當(dāng)前作用域之外的代碼中使用。發(fā)布對象的方式包括:非私用變量的引用、方法調(diào)用返回的引用、發(fā)布內(nèi)部類對象隱含外部類的引用等等。當(dāng)某個不應(yīng)該發(fā)布的對象被發(fā)布是,就被稱為泄露。
public class ThisEscape { private int status; public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); status = 1; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
由于內(nèi)部類的實例包含了對外部類實例的隱含引用,當(dāng)ThisEscape發(fā)布EventListener時,也隱含發(fā)布了ThisEscape實例本身。但在此時,變量status還沒有被初始化,造成了this引用在構(gòu)造函數(shù)中泄露??梢允褂靡粋€私有的構(gòu)造函數(shù)和一個公共的工廠方法,避免不正確的構(gòu)造過程:
public class SafeListener { private int status; private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; status = 1; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
3. 線程封閉
一種避免使用同步的方式就是不共享。如果僅在單線程內(nèi)訪問數(shù)據(jù),就不需要同步,這就被稱為線程封閉。線程封閉是程序設(shè)計中的考慮因素,必須在程序中實現(xiàn)。Java也提供了一些機制幫助維護線程封閉,比如局部變量和ThreadLocal
。
3.1 Ad-hoc線程封閉
Ad-hoc線程封閉是指,維護線程封閉性的職責(zé)完全由程序?qū)崿F(xiàn)來承擔(dān)。使用volatile變量是實現(xiàn)Ad-hoc線程封閉的一種方式,只要能保證只有單個線程對共享volatile變量執(zhí)行寫入操作,那么就可以安全低在這些變量上進行“讀取-修改-寫入”操作,volatile變量的可見性又保證了其他線程能夠看到最新的值。
Ad-hoc線程封閉是非常脆弱的,因此在程序中盡量少使用。在可能的情況下,使用其他線程封閉技術(shù),比如:棧封閉、ThreadLocal。
3.2 棧封閉
在棧封閉中,只能通過局部變量才能訪問對象。它們位于執(zhí)行線程的棧中,其他線程無法訪問到。即使這些對象是非線程安全的對象,它們?nèi)匀皇蔷€程安全的。然而,值得注意的是,只要編寫代碼的人才知道哪些對象是棧封閉的。如果沒有明確的說明,后續(xù)的維護人員很容易錯誤的泄露這些對象。
3.3 ThreadLocal類
使用ThreadLocal是一種更規(guī)范的線程封閉方式,它能是線程中的某個值與保存值的對象關(guān)聯(lián)起來。如下代碼,通過將JDBC的連接保存到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(); } }
從概念上看,你可以將ThreadLocal<T>視為包含了Map<Thread,T>對象,其中保存了特定于改線程的值,但ThreadLocal的實現(xiàn)并非如此。這些特定于線程的值保存在Thread對象中,當(dāng)線程終止后,這些值會作為垃圾被回收。
4. 不變性
如果某個對象在被創(chuàng)建后其狀態(tài)就不能被修改,那么這個對象就被稱為不可變對象。滿足同步需求的另一種方法就是使用不可變對象。不可變對象一定是線程安全的。當(dāng)滿足以下條件時,對象才是不可變的:
- 對象創(chuàng)建以后其狀態(tài)就不能改變
- 對象的所有域都是final類型
- 對象是正確創(chuàng)建的,在對象創(chuàng)建期間,this引用沒有泄露
public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } }
上述代碼中,盡管stooges對象是可變的,但在它構(gòu)造完成后無法對其修改。stooges是一個final類型的引用變量,因此所有的對象狀態(tài)都通過一個final域訪問。在構(gòu)造函數(shù)中,this引用不能被除了構(gòu)造函數(shù)之外的代碼訪問到。
4.1 final域
final類型的域是不能修改的,但如果final域所引用的對象是可變的,那么這些被引用的對象是可以修改的。final域的對象在構(gòu)造函數(shù)中不會被重排序,所以final域也能保證初始化過程的安全性。和“除非需要更高的可見性,否則應(yīng)將所有的域都聲明為私用域”一樣,“除非需要某個域是可變的,否則應(yīng)將其聲明為final域”也是一個良好的編程習(xí)慣。
4.2 使用volatile類型來發(fā)布不可變對象
因式分解Sevlet將執(zhí)行兩個原子操作:
- 更新緩存
- 通過判斷緩存中的數(shù)值是否等于請求的數(shù)值來決定是否直接讀取緩存中的結(jié)果
每當(dāng)需要一組相關(guān)數(shù)據(jù)以原子方式執(zhí)行某個操作時,就可以考慮創(chuàng)建一個不可變的類來包含這些數(shù)據(jù):
public class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } }
當(dāng)線程獲取了不可變對象的引用后,不必擔(dān)心另一個線程會修改對象的狀態(tài)。如果要更新這些變量,可以創(chuàng)建一個新的容器對象,但其他使用原有對象的線程仍然會看到對象處于一致的狀態(tài)。當(dāng)一個線程將volatile類型的cache設(shè)置為引用一個新的OneValueCache
時,其他線程就會立即看到新緩存的數(shù)據(jù):
public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } }
5 安全發(fā)布
5.1 不正確的發(fā)布
像這樣將對象引用保存到公有域中就是不安全的:
public Holder holder; public void initialize(){ holder = new Holder(42); }
由于存在可見性問題,其他線程看到的Holder對象將處于不一致的狀態(tài)。除了發(fā)布對象的線程外,其他線程可以看到Holder
域是一個失效值,因此將看到一個空引用或者之前的舊值。
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); } }
上述代碼,即使Holder對象被正確的發(fā)布,assertSanity
也有可能拋出AssertionError
。因為線程看到Holder引用的值是最新的,但由于重排序Holder狀態(tài)的值卻是時效的。
5.2 不可變對象與初始化安全性
即使在發(fā)布不可變對象的引用時沒有使用同步,也仍然可以安全地訪問該對象。任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發(fā)布這些對象時沒有使用同步。在沒有額外同步的情況下,也可以安全地訪問final類型的域。然而,如果final類型的域所指向的是可變對象,那么在訪問這些域所指向的對象的狀態(tài)時仍然需要同步。
5.3 安全發(fā)布的常用模式
要安全地發(fā)布一個對象,對象的引用以及對象的狀態(tài)必須同時對其他線程可見。一個正確構(gòu)造的對象可以通過以下方式來安全發(fā)布:
- 在靜態(tài)初始化函數(shù)里初始化一個對象引用。
- 將對象的引用保存到volatile類型的域或者AtomicReference對象中。
- 將對象的引用保存到某個正確構(gòu)造對象的final類型域中。
- 將對象的引用保存到一個由鎖保護的域中。
線程安全庫中的容器類提供了以下的安全發(fā)布保證:
- 通過將一個鍵或者值放入
Hashtable
、synchronizedMap
或者ConcurrentMap
中,可以安全地將它發(fā)布給任何從這些容器中訪問它的線程。 - 通過將某個對象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以將該對象安全地發(fā)布到任何從這些容器中訪問該對象的線程。
- 通過將某個對象放入BlockingQueue或者
ConcurrentLinkedQueue
中,可以將該對象安全地發(fā)布到任何從這些隊列中訪問該對象的線程。
5.4 事實不可變對象
如果對象從技術(shù)上來看是可變的,但其狀態(tài)在發(fā)布后不會再改變,那么把這種對象稱為事實不可變對象。在沒有額外的同步的情況下,任何線程都可以安全地使用被安全發(fā)布的事實不可變對象。例如維護一個Map對象,其中保存了每位用戶的最新登錄時間:
public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date());
如果Date對象的值在被放入Map后就不會改變,那么synchronizedMap
中的同步機制就足以使Date值被安全地發(fā)布,并且在訪問這些Date值時不需要額外的同步。
5.5 可變對象
對于可變對象,不僅在發(fā)布對象是需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保后續(xù)修改操作的可見性。對象的發(fā)布需求取決于它的可變性:
- 不可變對象可以通過任意機制來發(fā)布。
- 事實不可變對象必須通過安全方式來發(fā)布。
- 可變對象必須通過安全方式來發(fā)布,而且必須是線程安全的或者用某個鎖保護起來。
5.6 安全的共享對象
在并發(fā)程序中使用和共享對象時,可以使用一些實用的策略,包括:
- 線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只能由這個線程修改。
- 只讀共享。在沒有額外同步的情況下,共享的只讀對象可以由多個線程并發(fā)訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
- 線程安全共享。線程安全的對象在其內(nèi)部實現(xiàn)同步,因此多個線程可以通過對象的公共接口來進行訪問而不需要進一步的同步。
- 保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發(fā)布的并且由某個特定鎖保護的對象。
到此這篇關(guān)于Java并發(fā)編程之對象的共享的文章就介紹到這了,更多相關(guān)Java對象的共享內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot?@Async?注解如何實現(xiàn)方法異步
這篇文章主要介紹了springboot?@Async?注解如何實現(xiàn)方法異步,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Java語言實現(xiàn)簡單FTP軟件 FTP軟件主界面(4)
這篇文章主要為大家詳細介紹了Java語言實現(xiàn)簡單FTP軟件,F(xiàn)TP軟件主界面編寫的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03Admin - SpringBoot + Maven 多啟動環(huán)境配置實例詳解
這篇文章主要介紹了Admin - SpringBoot + Maven 多啟動環(huán)境配置,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03