摘要:本文探討并發(fā)中的其它問題線程安全可見性活躍性等等。當(dāng)閉鎖到達(dá)結(jié)束狀態(tài)時,門打開并允許所有線程通過。在從返回時被叫醒時,線程被放入鎖池,與其他線程競爭重新獲得鎖。
本文探討Java并發(fā)中的其它問題:線程安全、可見性、活躍性等等。
在行文之前,我想先推薦以下兩份資料,質(zhì)量很高:
極客學(xué)院-Java并發(fā)編程
讀書筆記-《Java并發(fā)編程實戰(zhàn)》
《Java并發(fā)編程實戰(zhàn)》中提到了太多的術(shù)語,比如各種XX性。而安全性我覺得這個概念并不妥。計算機(jī)術(shù)語中的線程安全大家一說就懂,但老是生造概念就不好了。又例如,活躍性,就是避免饑餓和死鎖唄!
線程安全問題就是多線程時結(jié)果受執(zhí)行順序影響,要解決就要讓相關(guān)操作具有原子性。這個上過操作系統(tǒng)原理的肯定都知道。至于原子性,不再解釋。
那么,Java給出了哪些工具來保證原子性和線程安全?
內(nèi)置鎖即synchronized關(guān)鍵字。內(nèi)置鎖可以作用在方法、代碼塊中,作用在方法時表示用該類的當(dāng)前實例(this)作為鎖給方法體加鎖。內(nèi)置鎖的實現(xiàn)是通過編譯器加入monitor_enter和montior_exit指令,在虛擬機(jī)遇到前者時嘗試獲取鎖,把鎖的計數(shù)器加1;遇到后者時,將鎖計數(shù)器減1,鎖計數(shù)器為0時,鎖被釋放。
內(nèi)置鎖一度是java中進(jìn)行同步的唯一方法,很多遺留方法還是使用了內(nèi)置鎖進(jìn)行同步,比如著名的Vector,Collections里面的同步包裝器(如Collections.synchronizedMap(hashmap))等。
關(guān)于它和Lock的比較,詳見此文。結(jié)論是,建議優(yōu)先使用synchronized來進(jìn)行同步。
顯式鎖顯式鎖的頂層接口為Lock,提供了ReenterantLock, ReadWriteLock等實現(xiàn)。常見用法如下:
Lock lock = new ReentrantLock(); ... lock.lock(); try { // 方法體 } ... finally { lock.unlock(); }
所謂可重入就是鎖的獲得是以線程為單位的,同一線程獲得鎖后可以重復(fù)進(jìn)入鎖。鎖會保存被持有的計數(shù)。
信號量、柵欄、閉鎖信號量:Semaphore,相當(dāng)于允許進(jìn)入數(shù)量大于1的鎖。
閉鎖:Latch,實現(xiàn)類CountDownLatch。相當(dāng)于一個門,閉鎖到達(dá)結(jié)束狀態(tài)前,門一直關(guān)著,所有線程都不能通過。當(dāng)閉鎖到達(dá)結(jié)束狀態(tài)時,門打開并允許所有線程通過。
柵欄:Barrier,所有線程都等待時才打開放行。
現(xiàn)代CPU支持一種CAS(Compare And Swap)指令,可以在一個指令內(nèi)完成設(shè)置和沖突檢測,從而實現(xiàn)了高效的原子性。CAS指令接受三個參數(shù)(v, expectedValue, newValue)。如果變量v的值和expectedValue相等,那么就將v賦值為newValue;如果和expectedValue不相等,就返回失敗。
為何CAS的效率更高?采用互斥同步策略的最主要問題就是進(jìn)行線程阻塞和喚醒所帶來的性能問題,因而這種同步又稱為阻塞同步,它屬于一種悲觀的并發(fā)策略(悲觀鎖),即線程獲得的是獨占鎖。獨占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在 CPU 轉(zhuǎn)換線程阻塞時會引起線程上下文切換,當(dāng)有很多線程競爭鎖的時候,會引起 CPU 頻繁的上下文切換(由此導(dǎo)致內(nèi)核態(tài)和用戶態(tài)切換)導(dǎo)致效率很低。
而基于沖突檢測(CAS)的樂觀并發(fā)策略,通俗地講就是先進(jìn)性操作,如果沒有其他線程爭用共享數(shù)據(jù),那操作就成功了,如果共享數(shù)據(jù)被爭用,產(chǎn)生了沖突,那就再進(jìn)行其他的補(bǔ)償措施(最常見的補(bǔ)償措施就是不斷地重試,直到試成功為止),這種樂觀的并發(fā)策略不需要把線程掛起,因此這種同步被稱為非阻塞同步。
Java 5.0之后才支持CAS,并用它實現(xiàn)了一些原子變量類,如AtomicInteger//AtomicReference等等。更重要的是,前面提到的所有鎖機(jī)制幾乎都使用了CAS來做性能優(yōu)化。
線程間協(xié)作這里的線程間協(xié)作是指通過一些機(jī)制使得線程可以彼此等待、喚醒,從而能夠合作。例如,在生產(chǎn)者-消費者模型中,如果生產(chǎn)者向隊列中放入一個新任務(wù),可以立刻喚醒一個等待在此的消費者,這便是協(xié)作。
首先是基于內(nèi)置鎖和Object類的wait()、notify()、notifyAll()方法。
在java中,每個對象都有兩個池,鎖池和等待池。
鎖池:要進(jìn)入synchronized同步塊的線程,如果此同步塊的鎖(是一個對象)被其他線程持有,則顯然線程不能執(zhí)行下去。線程將被放入該鎖對象的鎖池中,在鎖池中的線程都在競爭這個鎖。
等待池:調(diào)用了鎖對象的wait()方法后,就進(jìn)入了等待池。在等待池中的線程不去競爭鎖,而是等待被鎖對象的notify()或notifyAll()喚醒,之后再進(jìn)入鎖池,開始競爭鎖。
所以,鎖池中的線程相當(dāng)于睡著了,而等待池中的線程則進(jìn)入了第二層睡眠!(如果你看過《盜夢空間》的話~)
再來講這三個方法就好理解了:
Object.wait()
將當(dāng)前線程放到鎖的等待池,直到接到通知(其他線程調(diào)用 notify()方法或 notifyAll()方法)或被中斷。在調(diào)用 wait()之前,線程必須要獲得該對象鎖,即只能在同步塊中調(diào)用 wait()方法。進(jìn)入 wait()方法后,當(dāng)前線程釋放鎖。在從 wait()返回時(被叫醒時),線程被放入鎖池,與其他線程競爭重新獲得鎖。
Object.notify()
也必須在同步方法或同步塊中調(diào)用,用來“叫醒”鎖的等待池中的其他線程。如果有多個線程等待,則任意挑選出其中一個,扔到鎖池中,但不驚動其他同樣在等待被該對象notify的線程們。這里的“叫醒”只是叫醒第二層睡眠,還沒完全醒。
Object.notifyAll()
把所有的鎖等待池中的線程扔到鎖池中。
說了這么多,這幾個方法有什么用?還是生產(chǎn)者-消費者問題,隊列數(shù)據(jù)的正確性需要同步機(jī)制來確保,而兩個線程何時生產(chǎn),何時取走就需要線程間協(xié)作了。詳見此文。
最后,實際上這幾個方法已經(jīng)過時了。如果想實現(xiàn)等待阻塞的功能,應(yīng)該使用更好用的Lock和Condition,與前面的組合如出一轍。
關(guān)于Lock和Condition的例子,見此回答。
可見性與同步、volatile可見性指的是,一個變量被一個線程更改后,另一個線程在讀取時由于時間順序,可能得到的最新的有效值,也可能得到的是舊的無效值。
因此,同步的意義不僅僅在于寫,還在于讀。只要在讀的時候也進(jìn)行同步操作(加鎖),就肯定能保證可見性。
另一方面,使用volatile關(guān)鍵字可以實現(xiàn)輕量級的可見性。volatile關(guān)鍵字會禁止所修飾的變量被指令重排序和優(yōu)化成寄存器值從而不對所有線程可見。由于Java保證最低可見性(CPU設(shè)置一個變量會是個原子操作,不會出現(xiàn)設(shè)置到一半就被讀取,從而得到一個隨機(jī)值的情況),因而volatile可以實現(xiàn)非常高效的可見性。
但是volatile的局限也是有的:它只能用于賦值操作,如果是i++這種組合操作,結(jié)果依賴于之前的值,就不再能保證原子性了,因而無法保證準(zhǔn)確。這時,只能采用加鎖操作(或者CAS的沖突重試,總之要保證原子性)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/68534.html
摘要:發(fā)布的對象內(nèi)部狀態(tài)可能會破壞封裝性,使程序難以維持不變性條件。不變性線程安全性是不可變對象的固有屬性之一。可變對象必須通過安全方式來發(fā)布,并且必須是線程安全的或者有某個鎖保護(hù)起來。 線程的優(yōu)缺點 線程是系統(tǒng)調(diào)度的基本單位。線程如果使用得當(dāng),可以有效地降低程序的開發(fā)和維護(hù)等成本,同時提升復(fù)雜應(yīng)用程序的性能。多線程程序可以通過提高處理器資源的利用率來提升系統(tǒng)的吞吐率。與此同時,在線程的使用...
摘要:純分享直接上干貨操作系統(tǒng)并發(fā)支持進(jìn)程管理內(nèi)存管理文件系統(tǒng)系統(tǒng)進(jìn)程間通信網(wǎng)絡(luò)通信阻塞隊列數(shù)組有界隊列鏈表無界隊列優(yōu)先級有限無界隊列延時無界隊列同步隊列隊列內(nèi)存模型線程通信機(jī)制內(nèi)存共享消息傳遞內(nèi)存模型順序一致性指令重排序原則內(nèi)存語義線程 純分享 , 直接上干貨! 操作系統(tǒng)并發(fā)支持 進(jìn)程管理內(nèi)存管...
摘要:前言并發(fā)編程的目的是讓程序跑的更快,但并不是啟動更多的線程,這個程序就跑的更快。盡可能降低上下文切換的次數(shù),有助于提高并發(fā)效率。死鎖并發(fā)編程中的另一挑戰(zhàn)是死鎖,會造成系統(tǒng)功能不可用。 前言 并發(fā)編程的目的是讓程序跑的更快,但并不是啟動更多的線程,這個程序就跑的更快。有以下幾種挑戰(zhàn)。 挑戰(zhàn)及方案 上下文切換 單核CPU上執(zhí)行多線程任務(wù),通過給每個線程分配CPU時間片的方式來實現(xiàn)這個機(jī)制。...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識點總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時間和時間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識點總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時間和時間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 1232·2021-11-11 16:54
閱讀 883·2021-10-19 11:44
閱讀 1348·2021-09-22 15:18
閱讀 2455·2019-08-29 16:26
閱讀 2958·2019-08-29 13:57
閱讀 3102·2019-08-26 13:32
閱讀 1090·2019-08-26 11:58
閱讀 2339·2019-08-26 10:37