多线程:synchronize、volatile、Lock 的区别与用法
Java多線程之內(nèi)存可見性和原子性:Synchronized和Volatile的比較
??????在說(shuō)明Java多線程內(nèi)存可見性之前,先來(lái)簡(jiǎn)單了解一下Java內(nèi)存模型。
?????(1)Java所有變量都存儲(chǔ)在主內(nèi)存中
?????(2)每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程的使用到的變量副本(該副本就是主內(nèi)存中該變量的一份拷貝)
???(1)線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行,不能直接在主內(nèi)存中讀寫
???(2)不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量,線程間變量值的傳遞需要通過(guò)主內(nèi)存來(lái)完成。
線程1對(duì)共享變量的修改,要想被線程2及時(shí)看到,必須經(jīng)過(guò)如下2個(gè)過(guò)程:
???(1)把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中
???(2)將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中
可見性與原子性
???可見性:一個(gè)線程對(duì)共享變量的修改,更夠及時(shí)的被其他線程看到
???原子性:即不可再分了,不能分為多步操作。比如賦值或者return。比如"a = 1;"和 "returna;"這樣的操作都具有原子性。類似"a +=b"這樣的操作不具有原子性,在某些JVM中"a +=b"可能要經(jīng)過(guò)這樣三個(gè)步驟:
① 取出a和b
② 計(jì)算a+b
③ 將計(jì)算結(jié)果寫入內(nèi)存
(1)Synchronized:保證可見性和原子性
???Synchronized能夠?qū)崿F(xiàn)原子性和可見性;在Java內(nèi)存模型中,synchronized規(guī)定,線程在加鎖→先清空工作內(nèi)存→在主內(nèi)存中拷貝最新變量的副本到工作內(nèi)存→執(zhí)行完代碼→將更改后的共享變量的值刷新到主內(nèi)存中→釋放互斥鎖。
(2)Volatile:保證可見性,但不保證操作的原子性
???Volatile實(shí)現(xiàn)內(nèi)存可見性是通過(guò)store和load指令完成的;也就是對(duì)volatile變量執(zhí)行寫操作時(shí),會(huì)在寫操作后加入一條store指令,即強(qiáng)迫線程將最新的值刷新到主內(nèi)存中;而在讀操作時(shí),會(huì)加入一條load指令,即強(qiáng)迫從主內(nèi)存中讀入變量的值。但volatile不保證volatile變量的原子性,例如:
(3)Synchronized和Volatile的比較
????1)Synchronized保證內(nèi)存可見性和操作的原子性
? ? ? ? ? ? ?加鎖----清空內(nèi)存----在主存中拷貝最新副本----執(zhí)行+修改--------刷回主存-------釋放鎖
????2)Volatile只能保證內(nèi)存可見性
? ? ? ? ? a.每次讀取的時(shí)候都會(huì)CAS
? ? ? ? ?b.每次寫完都會(huì)store回主存
????3)Volatile不需要加鎖(忙等待,做自旋),比Synchronized更輕量級(jí),并不會(huì)阻塞線程(volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。)
????4)volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化(通過(guò)volatile保證了有序性),而synchronized標(biāo)記的變量可以被編譯器優(yōu)化(如編譯器重排序的優(yōu)化). (synchronized和Lock來(lái)保證有序性,很顯然,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性)所以根據(jù)before-happen原則,也可以保證有序性。
????5)volatile是變量修飾符,僅能用于變量,而synchronized是一個(gè)方法或塊的修飾符。
?????volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實(shí)現(xiàn)可見性。而對(duì)n=n+1,n++等操作時(shí),volatile關(guān)鍵字將失效,不能起到像synchronized一樣的線程同步(原子性)的效果。
?
?Java? Synchronize?和?Lock?的區(qū)別與用法
? ? java為此也提供了2種鎖機(jī)制,synchronized和lock。
一、synchronized和lock的用法區(qū)別
?
synchronized:在需要同步的對(duì)象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號(hào)中表示需要鎖的對(duì)象。
?
lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個(gè)線程中必須要使用一個(gè)ReentrantLock類做為對(duì)象才能保證鎖的生效。且在加鎖和解鎖處需要通過(guò)lock()和unlock()顯示指出。所以一般會(huì)在finally塊中寫unlock()以防死鎖。
?
用法區(qū)別比較簡(jiǎn)單,這里不贅述了,如果不懂的可以看看Java基本語(yǔ)法。
?
二、synchronized和lock性能區(qū)別
?
synchronized是托管給JVM執(zhí)行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因?yàn)檫@是一個(gè)重量級(jí)操作,需要調(diào)用操作接口,導(dǎo)致有可能加鎖消耗的系統(tǒng)時(shí)間比加鎖以外的操作還多。
但是到了Java1.6,發(fā)生了變化。synchronized在語(yǔ)義上很清晰,可以進(jìn)行很多優(yōu)化,有適應(yīng)自旋,鎖消除(消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)這種方式消除沒有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競(jìng)爭(zhēng)的情景,JVM會(huì)自動(dòng)將其鎖消除。),鎖粗化,輕量級(jí)鎖,偏向鎖等等。導(dǎo)致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支持synchronize,在未來(lái)的版本中還有優(yōu)化余地。
三、synchronized和lock用途區(qū)別
?
synchronized原語(yǔ)和ReentrantLock在一般情況下沒有什么區(qū)別,但是在非常復(fù)雜的同步應(yīng)用中,請(qǐng)考慮使用ReentrantLock,特別是遇到下面幾種需求的時(shí)候。
*(當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情。注意:synchronized不提供中斷)
?
1.某個(gè)線程在等待一個(gè)鎖的控制權(quán)的這段時(shí)間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock里面的Condition應(yīng)用,能夠控制notify哪個(gè)線程(所謂notify也就是notify等待這個(gè)加鎖對(duì)象的鎖池(entryset)里面的線程)
3.具有公平鎖功能,每個(gè)到來(lái)的線程都將排隊(duì)等候
?
先說(shuō)第一種情況,ReentrantLock的lock機(jī)制有2種,忽略中斷鎖和響應(yīng)中斷鎖,這給我們帶來(lái)了很大的靈活性。
比如:如果A、B2個(gè)線程去競(jìng)爭(zhēng)鎖,A線程得到了鎖,B線程等待,但是A線程這個(gè)時(shí)候?qū)嵲谟刑嗍虑橐幚?#xff0c;就是一直不返回,B線程可能就會(huì)等不及了,想中斷自己,不再等待這個(gè)鎖了,轉(zhuǎn)而處理其他事情。這個(gè)時(shí)候ReentrantLock就提供了2種機(jī)制,
第一,B線程中斷自己(或者別的線程中斷它),但是ReentrantLock不去響應(yīng),繼續(xù)讓B線程等待,你再怎么中斷,我全當(dāng)耳邊風(fēng)(synchronized原語(yǔ)就是如此)(也就是說(shuō),B在我的鎖池里,B如果想要中斷自己不想等待這把鎖,那么需要經(jīng)過(guò)我的響應(yīng),比如做一些引用計(jì)數(shù)的操作,才能中斷);第二,B線程中斷自己(或者別的線程中斷它),ReentrantLock處理了這個(gè)中斷,并且不再等待這個(gè)鎖的到來(lái),完全放棄。
?
這里來(lái)做個(gè)試驗(yàn),首先搞一個(gè)Buffer類,它有讀操作和寫操作,為了不讀到臟數(shù)據(jù),寫和讀都需要加鎖,我們先用synchronized原語(yǔ)來(lái)加鎖
我們期待“讀”這個(gè)線程能退出等待鎖,可是事與愿違,一旦讀這個(gè)線程發(fā)現(xiàn)自己得不到鎖,就一直開始等待了,就算它等死,也得不到鎖,因?yàn)閷懢€程要21億秒才能完成 T_T ,即使我們中斷它,它都不來(lái)響應(yīng)下,看來(lái)真的要等死了。這個(gè)時(shí)候,ReentrantLock給了一種機(jī)制讓我們來(lái)響應(yīng)中斷,讓“讀”能伸能屈,勇敢放棄對(duì)這個(gè)鎖的等待。我們來(lái)改寫B(tài)uffer這個(gè)類,就叫BufferInterruptibly吧,可中斷緩存。
這次“讀”線程接收到了lock.lockInterruptibly()中斷,并且有效處理了這個(gè)“異常”。
?
至于第二種情況,ReentrantLock可以與Condition的配合使用,Condition為ReentrantLock鎖的等待和釋放提供控制邏輯。
?
例如,使用ReentrantLock加鎖之后,可以通過(guò)它自身的Condition.await()方法釋放該鎖,線程在此等待Condition.signal()方法,然后繼續(xù)執(zhí)行下去。await方法需要放在while循環(huán)中,因此,在不同線程之間實(shí)現(xiàn)并發(fā)控制,還需要一個(gè)volatile的變量,boolean是原子性的變量。
調(diào)用spillDone.await()時(shí)可以釋放spillLock鎖,線程進(jìn)入阻塞狀態(tài),而等待其他線程的spillDone.signal()操作時(shí),就會(huì)喚醒線程,重新持有spillLock鎖。
?
這里可以看出,利用lock可以使我們多線程交互變得方便,而使用synchronized則無(wú)法做到這點(diǎn)。
?
最后呢,ReentrantLock這個(gè)類還提供了2種競(jìng)爭(zhēng)鎖的機(jī)制:公平鎖(先來(lái)后到原則,估計(jì)就是一個(gè)隊(duì)列性質(zhì))和非公平鎖(不分先后,估計(jì)就是一個(gè)類似于set)。
這2種機(jī)制的意思從字面上也能了解個(gè)大概:即對(duì)于多線程來(lái)說(shuō),公平鎖會(huì)依賴線程進(jìn)來(lái)的順序,后進(jìn)來(lái)的線程后獲得鎖。而非公平鎖的意思就是后進(jìn)來(lái)的鎖也可以和前邊等待鎖的線程同時(shí)競(jìng)爭(zhēng)鎖資源。對(duì)于效率來(lái)講,當(dāng)然是非公平鎖效率更高,因?yàn)楣芥i還要判斷是不是線程隊(duì)列的第一個(gè)才會(huì)讓線程獲得鎖。
這兩種排隊(duì)策略供我們自己選擇。
?
總結(jié)
? (1)synchronized與volatile的比較
? ? ? ? ?1)volatile比synchronized更輕量級(jí)。
? ? ? ? ?2)volatile沒有synchronized使用的廣泛。
? ? ? ? ?3)volatile不需要加鎖,比synchronized更輕量級(jí),不會(huì)阻塞線程。
? ? ? ? ?4)從內(nèi)存可見性角度看,volatile讀相當(dāng)于加鎖,volatile寫相當(dāng)于解鎖。
? ? ? ? ?5)synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無(wú)法保證原子性。
? ? ? ? ?6)volatile本身不保證獲取和設(shè)置操作的原子性,僅僅保持修改的可見性。但是java的內(nèi)存模型保證聲明為
volatile的long和double變量的get和set操作是原子的。
(2)補(bǔ)充1
? ? ? ? ?共享數(shù)據(jù)的訪問(wèn)權(quán)限都必須定義為private。一般是考慮安全性,對(duì)數(shù)據(jù)提供保護(hù),可以通過(guò)set()方法賦值,再
通過(guò)get()方法取值,這就是java封裝的思想。
? ? ? ? Java中對(duì)共享數(shù)據(jù)操作的并發(fā)控制是采用加鎖技術(shù)。
? ? ? ? Java中沒有提供檢測(cè)與避免死鎖的專門機(jī)制,但應(yīng)用程序員可以采用某些策略防止死鎖的發(fā)生。
? ? ? ? final也可以保證內(nèi)存可見性。
?(3)補(bǔ)充2
? ? ? ? x86系統(tǒng)對(duì)64位(long、double)變量的讀寫可能不是原子操作.
volatile本身不保證獲取和設(shè)置操作的原子性,僅僅保持修改的可見性。但是java的內(nèi)存模型保證聲明為volatile的long和double變量的get和set操作是原子的.兩次操作變一次操作!
? ? ? ? 因此:Java內(nèi)存模型允許JVM將沒有被volatile修飾的64位數(shù)據(jù)類型的讀寫操作劃分為兩次32位的讀寫操作來(lái)運(yùn)行。
? ? ? ? 導(dǎo)致問(wèn)題:有可能會(huì)出現(xiàn)讀取到半個(gè)變量的情況。
? ? ? ? 解決方法:加volatile關(guān)鍵字。
? ? ? ?不過(guò)現(xiàn)在也有很多系統(tǒng)實(shí)現(xiàn)將沒有使用volatile的64位long\double的變量也設(shè)置為原子操作
?(4)一個(gè)問(wèn)題
? ? ? ? 即使沒有保證可見性的措施,很多時(shí)候共享變量依然能夠在主內(nèi)存和工作內(nèi)存間得到及時(shí)的更新?
? ? ? ? ? 是的,不過(guò)要看當(dāng)前線程并發(fā)量
? ? ? ? 答:一般只有在短時(shí)間內(nèi)高并發(fā)的情況下才會(huì)出現(xiàn)變量得不到及時(shí)更新的情況,因?yàn)镃PU在執(zhí)行時(shí)會(huì)很快地刷新
緩存,所以一般情況下很難看到這種問(wèn)題。
? ? ? ? 慢了不就不會(huì)刷新了。。。CPU運(yùn)算快的話,在分配的時(shí)間片內(nèi)就能完成所有工作:工作內(nèi)從1->主內(nèi)存->工作
內(nèi)存2,然后這個(gè)線程就釋放CPU時(shí)間片,這樣一來(lái)就保證了數(shù)據(jù)的可見性。如果是慢了話CPU強(qiáng)行剝奪該線的資
源,分配給其它線程,該線程就需要等待CPU下次給該線程分配時(shí)間片,如果在這段時(shí)間內(nèi)有別的線程訪問(wèn)共享變
量,可見性就沒法保證了。
選擇比較
除非需要使用 ReentrantLock 的高級(jí)功能(中斷、公平鎖),否則優(yōu)先使用 synchronized。這是因?yàn)?synchronized 是 JVM 實(shí)現(xiàn)的一種鎖機(jī)制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用擔(dān)心沒有釋放鎖而導(dǎo)致死鎖問(wèn)題,因?yàn)?JVM 會(huì)確保鎖的釋放。
總結(jié)
以上是生活随笔為你收集整理的多线程:synchronize、volatile、Lock 的区别与用法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 多线程:synchronized
- 下一篇: 多线程:CAS