Java中synchronized正確使用方法解析
生活中隨處可見并行的例子,并行 顧名思義就是一起進(jìn)行的意思,同樣的程序在某些時候也需要并行來提高效率,在上一篇文章中我們了解了 Java 語言對緩存導(dǎo)致的可見性問題、編譯優(yōu)化導(dǎo)致的順序性問題的解決方法,下面我們就來看看 Java 中解決因線程切換導(dǎo)致的原子性問題的解決方案 -- 鎖 。
說到鎖我們并不陌生,日常工作中也可能經(jīng)常會用到,但是我們不能只停留在用的層面上,為什么要加鎖,不加鎖行不行,不行的話會導(dǎo)致哪些問題,這些都是在使用加鎖語句時我們需要考慮的。
來看一個使用 32 位的 CPU 寫 long 型變量需不需要加鎖的問題:
我們知道 long 型變量長度為 64 位,在 32 位 CPU 上寫 long 型變量至少需要拆分成 2 個步驟:一次寫 高 32 位,一次寫低 32 位。
對于單核 CPU 來說,同一時刻只有一個線程在執(zhí)行,禁止 CPU 中斷就意味著禁止線程切換,獲得 CPU 使用權(quán)的這個線程就會一直運行,所以 2 次寫操作要么同時都被執(zhí)行,要么都不被執(zhí)行,單核 CPU 是保證原子性的。
對于多核 CPU,同一時刻,一個線程在 CPU-1 上運行,另一個線程在 CPU-2 上運行,此時禁止 CPU 切換,只能保證 CPU 上有線程運行,并不能保證同一時刻只有一個線程運行,如果兩個線程同時都在寫高位,那么得出的結(jié)果可就不正確了。
所以,互斥修改共享變量這個條件非常重要,也就是說同一時刻只有一個線程在修改共享變量,只要保證這個條件,不論單核還是多核,操作就都是原子性的了。
一說到互斥、原子性,我們馬上就想到了代碼加鎖,沒錯加鎖是正確的選擇,但是怎么加呢? 要想知道怎么加鎖,首先我們要知道加鎖鎖的是什么以及我們想要保護(hù)的資源是什么,看下圖說說鎖的是什么,要保護(hù)的是什么呢?
圖中鎖的 M 資源,保護(hù)的也是 M 資源。
程序中的鎖與現(xiàn)實中的鎖也是類似的,每一把鎖都有自己要保護(hù)的資源,這是至關(guān)重要的,如圖保護(hù)資源 M 的鎖為 LM,就像我家大門的鎖保護(hù)我家,你家大門的鎖保護(hù)你家一樣,如果程序出現(xiàn)類似我家大門鎖保護(hù)你家的情況,那么就會導(dǎo)致詭異的并發(fā)問題了。
了解了鎖的是什么與保護(hù)的是什么之后,我們看看怎么加鎖的問題,還是用 count += 1 的例子,看代碼:
class Test{ long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }
分析一下,這段代碼中鎖的是當(dāng)前對象,要保護(hù)的資源是對象中的成員屬性 value,這樣的加鎖方式開啟10 個線程分別調(diào)用 10000次 addOne()方法,我們預(yù)期的結(jié)果是 value 最終會達(dá)到 100000,結(jié)果如何呢 ?
經(jīng)過測試,addOne() 不加 synchronized 結(jié)果會出現(xiàn)小于 100000 的情況,加上 synchronized 結(jié)果符合我們的預(yù)期,針對測試結(jié)果,簡要分析如下:
加鎖之后,線程之間是互斥的,也就是說同一時刻只有一個線程執(zhí)行,這樣就原子性可以保證了。
那么可見性呢?一個線程操作結(jié)束后另一個線程能獲取到上一個線程的操作結(jié)果嗎?答案是肯定的,這就跟我們上一章說的 happen before 原則聯(lián)系到一起了,“一個鎖的解鎖操作對另一個鎖的加鎖操作是可見的”,再結(jié)合傳遞性規(guī)則,一個鎖在解鎖前,對共享變量的修改,即解鎖前對共享變量修改 happen before 于 這個鎖的解鎖,這個鎖的解鎖操作 happen before于另一個鎖的加鎖。
所以,解鎖前對共享變量修改happen before于另一個鎖的加鎖,也就是說解鎖前對共享變量修改對于另一個鎖的加鎖是可見的。
到這一切看似還挺完美,其實我們忽略了 get() 方法,多線程操作 get() 方法會是安全的嗎?在沒有任何前提操作的情況下,直接調(diào)用 get() 方法當(dāng)然沒問題,就是取值又不涉及修改。但是如果在執(zhí)行 addOne() 方法后調(diào)用呢?顯然,這時候 value 值的修改對 get() 方法是不可見的,happen before 中只說了鎖的規(guī)則,這里要想保證可見性,對 get()方法也需要加上一把鎖。代碼如下:
class Test{ long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; } }
這里我們用同一把鎖,保護(hù)了共享資源 value。說到這,我們根據(jù)資源關(guān)系來將使用鎖的情況分為兩種:
保護(hù)沒有關(guān)系的多個資源
保護(hù)有關(guān)系的多個資源
對于 1 的情況,由于屬性之間沒有關(guān)系,每個資源都用一把鎖來控制,例如修改賬戶的密碼、修改余額操作,密碼與余額是沒有關(guān)系的資源,分別用兩把鎖來控制即可,這種鎖叫做細(xì)粒度鎖,使用不同的鎖對受保護(hù)的資源進(jìn)行精細(xì)化管理,可以提升性能。
對于 2 的情況 ,則需要粒度更大的鎖去保護(hù)多個資源,看下面這段代碼:
class Account { private int balance; // 轉(zhuǎn)賬 synchronized void transfer( Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
乍一看,沒問題,轉(zhuǎn)賬操作加了鎖,妥妥的。其實則不然,看圖就明白了:
現(xiàn)在這就是"用我家鎖鎖了你家"的典型例子,這時候臨界區(qū)有多個資源,我們應(yīng)該使用更大粒度的鎖,看看這樣改怎么樣:
class Account { private int balance; // 轉(zhuǎn)賬 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
這里我們用 Account.class 作為更大粒度的鎖是可行的, class 就是我們常說的 “類模板”,在 JVM 中只會加載一次,所以所有 Account 對象的類模板都是相同的,這樣就能夠保證用一把大鎖鎖住了有關(guān)系的共享資源。
問題是解決了,仔細(xì)一想,如果用 Account.class 作為鎖,那豈不是所有的轉(zhuǎn)賬操作都是串行了,這樣肯定是不行的,生活中轉(zhuǎn)賬肯定也不是串行的,如果串行那效率真的是很太差了。
正確的方式應(yīng)該是這樣的:
class Account { //靜態(tài)屬性 替代 Account.class 作為一把大鎖 private static Object lock = new Object(); private int balance; // 轉(zhuǎn)賬 void transfer(Account target, int amt){ synchronized(lock) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
這樣一改,效率就上來了,問題也解決了,實際在開發(fā)中我們這也是我們最常用的加鎖的方式,使用靜態(tài)成員屬性作為鎖去保護(hù)有關(guān)系的多個資源。
總結(jié):
我們從導(dǎo)致并發(fā) bug 的原子性問題解決辦法---加鎖入手,了解了常規(guī)加鎖方式背后的邏輯---鎖的是什么與保護(hù)的是什么,與加鎖后變量的傳遞性規(guī)則,到最后不同資源關(guān)系對應(yīng)著不同的加鎖方式---細(xì)粒度鎖,粗粒度鎖。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot+redis+Interceptor+自定義annotation實現(xiàn)接口自動冪等
本篇文章給大家介紹了使用springboot和攔截器、redis來優(yōu)雅的實現(xiàn)接口冪等,對于冪等在實際的開發(fā)過程中是十分重要的,因為一個接口可能會被無數(shù)的客戶端調(diào)用,如何保證其不影響后臺的業(yè)務(wù)處理,如何保證其只影響數(shù)據(jù)一次是非常重要的,感興趣的朋友跟隨小編一起看看吧2019-07-07spring AOP的After增強(qiáng)實現(xiàn)方法實例分析
這篇文章主要介紹了spring AOP的After增強(qiáng)實現(xiàn)方法,結(jié)合實例形式分析了spring面向切面AOP的After增強(qiáng)實現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2020-01-01ActiveMQ結(jié)合Spring收發(fā)消息的示例代碼
這篇文章主要介紹了ActiveMQ結(jié)合Spring收發(fā)消息的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10Java中new關(guān)鍵字和newInstance方法的區(qū)別分享
在初始化一個類,生成一個實例的時候,newInstance()方法和new關(guān)鍵字除了一個是方法一個是關(guān)鍵字外,最主要的區(qū)別是創(chuàng)建對象的方式不同2013-07-07SpringBoot可視化監(jiān)控的具體應(yīng)用
最近越發(fā)覺得,任何一個系統(tǒng)上線,運維監(jiān)控都太重要了,本文介紹了SpringBoot可視化監(jiān)控的具體應(yīng)用,分享給大家,有興趣的同學(xué)可以參考一下2021-06-06Spring Security攔截器引起Java CORS跨域失敗的問題及解決
這篇文章主要介紹了Spring Security攔截器引起Java CORS跨域失敗的問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07