舉例解析Java多線(xiàn)程編程中需要注意的一些關(guān)鍵點(diǎn)
1. 同步方法或同步代碼塊?
您可能偶爾會(huì)思考是否要同步化這個(gè)方法調(diào)用,還是只同步化該方法的線(xiàn)程安全子集。在這些情況下,知道 Java 編譯器何時(shí)將源代碼轉(zhuǎn)化為字節(jié)代碼會(huì)很有用,它處理同步方法和同步代碼塊的方式完全不同。
當(dāng) JVM 執(zhí)行一個(gè)同步方法時(shí),執(zhí)行中的線(xiàn)程識(shí)別該方法的 method_info 結(jié)構(gòu)是否有 ACC_SYNCHRONIZED 標(biāo)記設(shè)置,然后它自動(dòng)獲取對(duì)象的鎖,調(diào)用方法,最后釋放鎖。如果有異常發(fā)生,線(xiàn)程自動(dòng)釋放鎖。
另一方面,同步化一個(gè)方法塊會(huì)越過(guò) JVM 對(duì)獲取對(duì)象鎖和異常處理的內(nèi)置支持,要求以字節(jié)代碼顯式寫(xiě)入功能。如果您使用同步方法讀取一個(gè)方法的字節(jié)代碼,就會(huì)看到有十幾個(gè)額外的操作用于管理這個(gè)功能。清單 1 展示用于生成同步方法和同步代碼塊的調(diào)用:
清單 1. 兩種同步化方法
package com.geekcap; public class SynchronizationExample { private int i; public synchronized int synchronizedMethodGet() { return i; } public int synchronizedBlockGet() { synchronized( this ) { return i; } } }
synchronizedMethodGet() 方法生成以下字節(jié)代碼:
0: aload_0 1: getfield 2: nop 3: iconst_m1 4: ireturn
這里是來(lái)自 synchronizedBlockGet() 方法的字節(jié)代碼:
0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: getfield 6: nop 7: iconst_m1 8: aload_1 9: monitorexit 10: ireturn 11: astore_2 12: aload_1 13: monitorexit 14: aload_2 15: athrow
創(chuàng)建同步代碼塊產(chǎn)生了 16 行的字節(jié)碼,而創(chuàng)建同步方法僅產(chǎn)生了 5 行。
回頁(yè)首
2. ThreadLocal 變量
如果您想為一個(gè)類(lèi)的所有實(shí)例維持一個(gè)變量的實(shí)例,將會(huì)用到靜態(tài)類(lèi)成員變量。如果您想以線(xiàn)程為單位維持一個(gè)變量的實(shí)例,將會(huì)用到線(xiàn)程局部變量。ThreadLocal 變量與常規(guī)變量的不同之處在于,每個(gè)線(xiàn)程都有其各自初始化的變量實(shí)例,這通過(guò) get() 或 set() 方法予以評(píng)估。
比方說(shuō)您在開(kāi)發(fā)一個(gè)多線(xiàn)程代碼跟蹤器,其目標(biāo)是通過(guò)您的代碼惟一標(biāo)識(shí)每個(gè)線(xiàn)程的路徑。挑戰(zhàn)在于,您需要跨多個(gè)線(xiàn)程協(xié)調(diào)多個(gè)類(lèi)中的多個(gè)方法。如果沒(méi)有 ThreadLocal,這會(huì)是一個(gè)復(fù)雜的問(wèn)題。當(dāng)一個(gè)線(xiàn)程開(kāi)始執(zhí)行時(shí),它需要生成一個(gè)惟一的令牌來(lái)在跟蹤器中識(shí)別它,然后將這個(gè)惟一的令牌傳遞給跟蹤中的每個(gè)方法。
使用 ThreadLocal,事情就變得簡(jiǎn)單多了。線(xiàn)程在開(kāi)始執(zhí)行時(shí)初始化線(xiàn)程局部變量,然后通過(guò)每個(gè)類(lèi)的每個(gè)方法訪(fǎng)問(wèn)它,保證變量將僅為當(dāng)前執(zhí)行的線(xiàn)程托管跟蹤信息。在執(zhí)行完成之后,線(xiàn)程可以將其特定的蹤跡傳遞給一個(gè)負(fù)責(zé)維護(hù)所有跟蹤的管理對(duì)象。
當(dāng)您需要以線(xiàn)程為單位存儲(chǔ)變量實(shí)例時(shí),使用 ThreadLocal 很有意義。
3. Volatile 變量
我估計(jì),大約有一半的 Java 開(kāi)發(fā)人員知道 Java 語(yǔ)言包含 volatile 關(guān)鍵字。當(dāng)然,其中只有 10% 知道它的確切含義,有更少的人知道如何有效使用它。簡(jiǎn)言之,使用 volatile 關(guān)鍵字識(shí)別一個(gè)變量,意味著這個(gè)變量的值會(huì)被不同的線(xiàn)程修改。要完全理解 volatile關(guān)鍵字的作用,首先應(yīng)當(dāng)理解線(xiàn)程如何處理非易失性變量。
為了提高性能,Java 語(yǔ)言規(guī)范允許 JRE 在引用變量的每個(gè)線(xiàn)程中維護(hù)該變量的一個(gè)本地副本。您可以將變量的這些 “線(xiàn)程局部” 副本看作是與緩存類(lèi)似,在每次線(xiàn)程需要訪(fǎng)問(wèn)變量的值時(shí)幫助它避免檢查主存儲(chǔ)器。
不過(guò)看看在下面場(chǎng)景中會(huì)發(fā)生什么:兩個(gè)線(xiàn)程啟動(dòng),第一個(gè)線(xiàn)程將變量 A 讀取為 5,第二個(gè)線(xiàn)程將變量 A 讀取為 10。如果變量 A 從 5 變?yōu)?10,第一個(gè)線(xiàn)程將不會(huì)知道這個(gè)變化,因此會(huì)擁有錯(cuò)誤的變量 A 的值。但是如果將變量 A 標(biāo)記為 volatile,那么不管線(xiàn)程何時(shí)讀取 A 的值,它都會(huì)回頭查閱 A 的原版拷貝并讀取當(dāng)前值。
如果應(yīng)用程序中的變量將不發(fā)生變化,那么一個(gè)線(xiàn)程局部緩存比較行得通。不然,知道 volatile 關(guān)鍵字能為您做什么會(huì)很有幫助。
4. 易失性變量與同步化
如果一個(gè)變量被聲明為 volatile,這意味著它預(yù)計(jì)會(huì)由多個(gè)線(xiàn)程修改。當(dāng)然,您會(huì)希望 JRE 會(huì)為易失性變量施加某種形式的同步。幸運(yùn)的是,JRE 在訪(fǎng)問(wèn)易失性變量時(shí)確實(shí)隱式地提供同步,但是有一條重要提醒:讀取易失性變量是同步的,寫(xiě)入易失性變量也是同步的,但非原子操作不同步。
這表示下面的代碼不是線(xiàn)程安全的:
myVolatileVar++;
上一條語(yǔ)句也可寫(xiě)成:
int temp = 0; synchronize( myVolatileVar ) { temp = myVolatileVar; } temp++; synchronize( myVolatileVar ) { myVolatileVar = temp; }
換言之,如果一個(gè)易失性變量得到更新,這樣其值就會(huì)在底層被讀取、修改并分配一個(gè)新值,結(jié)果將是一個(gè)在兩個(gè)同步操作之間執(zhí)行的非線(xiàn)程安全操作。然后您可以決定是使用同步化還是依賴(lài)于 JRE 的支持來(lái)自動(dòng)同步易失性變量。更好的方法取決于您的用例:如果分配給易失性變量的值取決于當(dāng)前值(比如在一個(gè)遞增操作期間),要想該操作是線(xiàn)程安全的,那么您必須使用同步化。
5. 原子字段更新程序
在一個(gè)多線(xiàn)程環(huán)境中遞增或遞減一個(gè)原語(yǔ)類(lèi)型時(shí),使用在 java.util.concurrent.atomic 包中找到的其中一個(gè)新原子類(lèi)比編寫(xiě)自己的同步代碼塊要好得多。原子類(lèi)確保某些操作以線(xiàn)程安全方式被執(zhí)行,比如遞增和遞減一個(gè)值,更新一個(gè)值,添加一個(gè)值。原子類(lèi)列表包括 AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray 等等。
使用原子類(lèi)的難題在于,所有類(lèi)操作,包括 get、set 和一系列 get-set 操作是以原子態(tài)呈現(xiàn)的。這表示,不修改原子變量值的 read和 write 操作是同步的,不僅僅是重要的 read-update-write 操作。如果您希望對(duì)同步代碼的部署進(jìn)行更多細(xì)粒度控制,那么解決方案就是使用一個(gè)原子字段更新程序。
使用原子更新
像 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 之類(lèi)的原子字段更新程序基本上是應(yīng)用于易失性字段的封裝器。Java 類(lèi)庫(kù)在內(nèi)部使用它們。雖然它們沒(méi)有在應(yīng)用程序代碼中得到廣泛使用,但是也沒(méi)有不能使用它們的理由。
清單 2 展示一個(gè)有關(guān)類(lèi)的示例,該類(lèi)使用原子更新來(lái)更改某人正在讀取的書(shū)目:
清單 2. Book 類(lèi)
package com.geeckap.atomicexample; public class Book { private String name; public Book() { } public Book( String name ) { this.name = name; } public String getName() { return name; } public void setName( String name ) { this.name = name; } }
Book 類(lèi)僅是一個(gè) POJO(Java 原生類(lèi)對(duì)象),擁有一個(gè)單一字段:name。
清單 3. MyObject 類(lèi)
package com.geeckap.atomicexample; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** * * @author shaines */ public class MyObject { private volatile Book whatImReading; private static final AtomicReferenceFieldUpdater<MyObject,Book> updater = AtomicReferenceFieldUpdater.newUpdater( MyObject.class, Book.class, "whatImReading" ); public Book getWhatImReading() { return whatImReading; } public void setWhatImReading( Book whatImReading ) { //this.whatImReading = whatImReading; updater.compareAndSet( this, this.whatImReading, whatImReading ); } }
正如您所期望的,清單 3 中的 MyObject 類(lèi)通過(guò) get 和 set 方法公開(kāi)其 whatAmIReading 屬性,但是 set 方法所做的有點(diǎn)不同。它不僅僅將其內(nèi)部 Book 引用分配給指定的 Book(這將使用 清單 3 中注釋出的代碼來(lái)完成),而是使用一個(gè)AtomicReferenceFieldUpdater。
AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater 的 Javadoc 將其定義為:
對(duì)指定類(lèi)的指定易失性引用字段啟用原子更新的一個(gè)基于映像的實(shí)用程序。該類(lèi)旨在用于這樣的一個(gè)原子數(shù)據(jù)結(jié)構(gòu)中:即同一節(jié)點(diǎn)的若干引用字段獨(dú)立地得到原子更新。
在 清單 3 中,AtomicReferenceFieldUpdater 由一個(gè)對(duì)其靜態(tài) newUpdater 方法的調(diào)用創(chuàng)建,該方法接受三個(gè)參數(shù):
包含字段的對(duì)象的類(lèi)(在本例中為 MyObject)
將得到原子更新的對(duì)象的類(lèi)(在本例中是 Book)
將經(jīng)過(guò)原子更新的字段的名稱(chēng)
這里真正的價(jià)值在于,getWhatImReading 方法未經(jīng)任何形式的同步便被執(zhí)行,而 setWhatImReading 是作為一個(gè)原子操作執(zhí)行的。
清單 4 展示如何使用 setWhatImReading() 方法并斷定值的變動(dòng)是正確的:
清單 4. 演習(xí)原子更新的測(cè)試用例
package com.geeckap.atomicexample; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class AtomicExampleTest { private MyObject obj; @Before public void setUp() { obj = new MyObject(); obj.setWhatImReading( new Book( "Java 2 From Scratch" ) ); } @Test public void testUpdate() { obj.setWhatImReading( new Book( "Pro Java EE 5 Performance Management and Optimization" ) ); Assert.assertEquals( "Incorrect book name", "Pro Java EE 5 Performance Management and Optimization", obj.getWhatImReading().getName() ); } }
結(jié)束語(yǔ)
多線(xiàn)程編程永遠(yuǎn)充滿(mǎn)了挑戰(zhàn),但是隨著 Java 平臺(tái)的演變,它獲得了簡(jiǎn)化一些多線(xiàn)程編程任務(wù)的支持。在本文中,我討論了關(guān)于在 Java 平臺(tái)上編寫(xiě)多線(xiàn)程應(yīng)用程序您可能不知道的 5 件事,包括同步化方法與同步化代碼塊之間的不同,為每個(gè)線(xiàn)程存儲(chǔ)運(yùn)用ThreadLocal 變量的價(jià)值,被廣泛誤解的 volatile 關(guān)鍵字(包括依賴(lài)于 volatile 滿(mǎn)足同步化需求的危險(xiǎn)),以及對(duì)原子類(lèi)的錯(cuò)雜之處的一個(gè)簡(jiǎn)要介紹。參見(jiàn) 參考資料 部分了解更多內(nèi)容。
相關(guān)文章
SpringBoot?SpringSecurity?JWT實(shí)現(xiàn)系統(tǒng)安全策略詳解
Spring?Security是Spring的一個(gè)核心項(xiàng)目,它是一個(gè)功能強(qiáng)大且高度可定制的認(rèn)證和訪(fǎng)問(wèn)控制框架。它提供了認(rèn)證和授權(quán)功能以及抵御常見(jiàn)的攻擊,它已經(jīng)成為保護(hù)基于spring的應(yīng)用程序的事實(shí)標(biāo)準(zhǔn)2022-11-11spring?cloud?eureka?服務(wù)啟動(dòng)失敗的原因分析及解決方法
這篇文章主要介紹了spring?cloud?eureka?服務(wù)啟動(dòng)失敗的原因解析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03Spring?Cloud灰度部署實(shí)現(xiàn)過(guò)程詳解
這篇文章主要為大家介紹了Spring?Cloud灰度部署實(shí)現(xiàn)過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Spring中的SpringData詳細(xì)說(shuō)明
這篇文章主要介紹了Spring中的SpringData詳細(xì)說(shuō)明,Spring Data 是Spring 的一個(gè)子項(xiàng)目, 旨在統(tǒng)一和簡(jiǎn)化對(duì)各類(lèi)型持久化存儲(chǔ), 而不拘泥于是關(guān)系型數(shù)據(jù)庫(kù)還是NoSQL 數(shù)據(jù)存儲(chǔ),需要的朋友可以參考下2023-11-11MyBatis?多表聯(lián)合查詢(xún)及優(yōu)化方法
大家都知道Hibernate 是全自動(dòng)的數(shù)據(jù)庫(kù)持久層框架,它可以通過(guò)實(shí)體來(lái)映射數(shù)據(jù)庫(kù),通過(guò)設(shè)置一對(duì)多、多對(duì)一、一對(duì)一、多對(duì)多的關(guān)聯(lián)來(lái)實(shí)現(xiàn)聯(lián)合查詢(xún),接下來(lái)通過(guò)本文給大家介紹MyBatis?多表聯(lián)合查詢(xún)及優(yōu)化,需要的朋友可以參考下2022-08-08舉例解析Java多線(xiàn)程編程中需要注意的一些關(guān)鍵點(diǎn)
這篇文章主要介紹了Java多線(xiàn)程編程中需要注意的一些關(guān)鍵點(diǎn),包括ThreadLocal變量與原子更新等一些深層次的內(nèi)容,需要的朋友可以參考下2015-11-11淺談兩個(gè)jar包中包含完全相同的包名和類(lèi)名的加載問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談兩個(gè)jar包中包含完全相同的包名和類(lèi)名的加載問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09深入解析反編譯字節(jié)碼文件中的代碼邏輯JVM中的String操作
這篇文章主要介紹了深入解析反編譯字節(jié)碼文件中的代碼邏輯JVM中的String操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10關(guān)于mybatis if else if 條件判斷SQL片段表達(dá)式取值和拼接問(wèn)題
這篇文章主要介紹了mybatis if else if 條件判斷SQL片段表達(dá)式取值和拼接,文章通過(guò)自己真實(shí)使用的例子給大家詳細(xì)介紹,需要的朋友可以參考下2021-09-09