1踪区、Java內(nèi)存模型(JMM)
從抽象的角度來看叹阔,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中师妙,每個線程都有一個私有的本地內(nèi)存(Local Memory)诗祸,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本血久。本地內(nèi)存是JMM的一個抽象概念突照,并不真實存在。它涵蓋了緩存氧吐、寫緩沖區(qū)讹蘑、寄存器以及其他的硬件和編譯器優(yōu)化。
2筑舅、可見性
- 可見性是指當多個線程訪問同一個變量時座慰,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值翠拣。
- 由于線程對變量的所有操作都必須在工作內(nèi)存中進行版仔,而不能直接讀寫主內(nèi)存中的變量,那么對于共享變量V误墓,它們首先是在自己的工作內(nèi)存蛮粮,之后再同步到主內(nèi)存∮派眨可是并不會及時的刷到主存中蝉揍,而是會有一定時間差。很明顯畦娄,這個時候線程 A 對變量 V 的操作對于線程 B 而言就不具備可見性了 。
- 要解決共享對象可見性這個問題弊仪,我們可以使用volatile關(guān)鍵字或者是加鎖熙卡。
3、原子性
- 原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷励饵,要么就都不執(zhí)行驳癌。
- 我們都知道CPU資源的分配都是以線程為單位的,并且是分時調(diào)用,操作系統(tǒng)允許某個進程執(zhí)行一小段時間,例如 50 毫秒役听,過了 50 毫秒操作系統(tǒng)就會重新選擇一個進程來執(zhí)行(我們稱為“任務(wù)切換”)颓鲜,這個 50 毫秒稱為“時間片”。而任務(wù)的切換大多數(shù)是在時間片段結(jié)束以后,
- 那么線程切換為什么會帶來bug呢典予?因為操作系統(tǒng)做任務(wù)切換甜滨,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意瘤袖,是 CPU 指令衣摩,CPU 指令,CPU 指令捂敌,而不是高級語言里的一條語句艾扮。比如count++既琴,在java里就是一句話,但高級語言里一條語句往往需要多條 CPU 指令完成泡嘴。其實count++包含了三個CPU指令甫恩!
4、volatile特性
可以把對volatile變量的單個讀/寫酌予,看成是使用同一個鎖對這些單個讀/寫操作做了同步
可以看成
- 所以volatile變量自身具有下列特性:
- 可見性磺箕。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入霎终。
- 原子性:對任意單個volatile變量的讀/寫具有原子性滞磺,但類似于volatile++這種復(fù)合操作不具有原子性。
- volatile雖然能保證執(zhí)行完及時把變量刷到主內(nèi)存中莱褒,但對于count++這種非原子性击困、多指令的情況,由于線程切換广凸,線程A剛把count=0加載到工作內(nèi)存阅茶,線程B就可以開始工作了,這樣就會導(dǎo)致線程A和B執(zhí)行完的結(jié)果都是1谅海,都寫到主內(nèi)存中脸哀,主內(nèi)存的值還是1不是2
5、volatile的實現(xiàn)原理
- 通過對OpenJDK中的unsafe.cpp源碼的分析扭吁,會發(fā)現(xiàn)被volatile關(guān)鍵字修飾的變量會存在一個“l(fā)ock:”的前綴撞蜂。
- Lock前綴,Lock不是一種內(nèi)存屏障侥袜,但是它能完成類似內(nèi)存屏障的功能蝌诡。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖枫吧。
- 同時該指令會將當前處理器緩存行的數(shù)據(jù)直接寫會到系統(tǒng)內(nèi)存中浦旱,且這個寫回內(nèi)存的操作會使在其他CPU里緩存了該地址的數(shù)據(jù)無效。
6九杂、synchronized的實現(xiàn)原理
- Synchronized在JVM里的實現(xiàn)都是基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步颁湖,雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)例隆。
- 對同步塊甥捺,MonitorEnter指令插入在同步代碼塊的開始位置,當代碼執(zhí)行到該指令時裳擎,將會嘗試獲取該對象Monitor的所有權(quán)涎永,即嘗試獲得該對象的鎖,而monitorExit指令則插入在方法結(jié)束處和異常處,JVM保證每個MonitorEnter必須有對應(yīng)的MonitorExit羡微。
- 對同步方法谷饿,從同步方法反編譯的結(jié)果來看,方法的同步并沒有通過指令monitorenter和monitorexit來實現(xiàn)妈倔,相對于普通方法博投,其常量池中多了ACC_SYNCHRONIZED標示符。
- JVM就是根據(jù)該標示符來實現(xiàn)方法的同步的:當方法被調(diào)用時盯蝴,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置毅哗,如果設(shè)置了,執(zhí)行線程將先獲取monitor捧挺,獲取成功之后才能執(zhí)行方法體虑绵,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間闽烙,其他任何線程都無法再獲得同一個monitor對象翅睛。
- synchronized使用的鎖是存放在Java對象頭里面,
具體位置是對象頭里面的MarkWord黑竞,MarkWord里默認數(shù)據(jù)是存儲對象的HashCode等信息捕发,
但是會隨著對象的運行改變而發(fā)生變化,不同的鎖狀態(tài)對應(yīng)著不同的記錄存儲方式
7很魂、自旋鎖
原理
- 自旋鎖原理非常簡單扎酷,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進入阻塞掛起狀態(tài)遏匆,它們只需要等一等(自旋)法挨,等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗幅聘。
但是線程自旋是需要消耗CPU的坷剧,說白了就是讓CPU在做無用功,線程不能一直占用CPU自旋做無用功喊暖,所以需要設(shè)定一個自旋等待的最大時間。
如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖撕瞧,就會導(dǎo)致其它爭用鎖的線程在最大等待時間內(nèi)還是獲取不到鎖陵叽,這時爭用線程會停止自旋進入阻塞狀態(tài)。
自旋鎖的優(yōu)缺點
- 自旋鎖盡可能的減少線程的阻塞丛版,這對于鎖的競爭不激烈巩掺,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起操作的消耗页畦!
但是如果鎖的競爭激烈胖替,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用cpu做無用功独令,占著XX不XX端朵,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要cup的線程又不能獲取到cpu燃箭,造成cpu的浪費冲呢。
自旋鎖時間閾值
- 自旋鎖的目的是為了占著CPU的資源不釋放,等到獲取到鎖立即進行處理招狸。但是如何去選擇自旋的執(zhí)行時間呢敬拓?如果自旋執(zhí)行時間太長,會有大量的線程處于自旋狀態(tài)占用CPU資源裙戏,進而會影響整體系統(tǒng)的性能乘凸。因此自旋次數(shù)很重要
- JVM對于自旋次數(shù)的選擇,jdk1.5默認為10次累榜,在1.6引入了適應(yīng)性自旋鎖营勤,適應(yīng)性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定信柿,基本認為一個線程上下文切換的時間是最佳的一個時間冀偶。
JDK1.6中-XX:+UseSpinning開啟自旋鎖; JDK1.7后渔嚷,去掉此參數(shù)进鸠,由jvm控制;
8形病、鎖的狀態(tài)
一共有四種狀態(tài)客年,無鎖狀態(tài),偏向鎖狀態(tài)漠吻,輕量級鎖狀態(tài)和重量級鎖狀態(tài)量瓜,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級途乃,目的是為了提高獲得鎖和釋放鎖的效率绍傲。
偏向鎖
引入背景:大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得耍共,為了讓線程獲得鎖的代價更低而引入了偏向鎖烫饼,減少不必要的CAS操作。
偏向鎖试读,顧名思義杠纵,它會偏向于第一個訪問鎖的線程,如果在運行過程中钩骇,同步鎖只有一個線程訪問比藻,不存在多線程爭用的情況铝量,則線程是不需要觸發(fā)同步的,減少加鎖/解鎖的一些CAS操作(比如等待隊列的一些CAS操作)银亲,這種情況下慢叨,就會給線程加一個偏向鎖。 如果在運行過程中群凶,遇到了其他線程搶占鎖插爹,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖请梢,將鎖恢復(fù)到標準的輕量級鎖赠尾。它通過消除資源無競爭情況下的同步原語,進一步提高了程序的運行性能毅弧。
- 偏向鎖獲取過程:
步驟1气嫁、 訪問Mark Word中偏向鎖的標識是否設(shè)置成1,鎖標志位是否為01够坐,確認為可偏向狀態(tài)寸宵。
步驟2、 如果為可偏向狀態(tài)元咙,則測試線程ID是否指向當前線程梯影,如果是,進入步驟5庶香,否則進入步驟3甲棍。
步驟3、 如果線程ID并未指向當前線程赶掖,則通過CAS操作競爭鎖感猛。如果競爭成功,則將Mark Word中線程ID設(shè)置為當前線程ID奢赂,然后執(zhí)行5陪白;如果競爭失敗,執(zhí)行4膳灶。
步驟4咱士、 如果CAS獲取偏向鎖失敗,則表示有競爭轧钓。當?shù)竭_全局安全點(safepoint)時獲得偏向鎖的線程被掛起司致,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼聋迎。(撤銷偏向鎖的時候會導(dǎo)致stop the word)
步驟5、 執(zhí)行同步代碼枣耀。