已排版:https://blog.csdn.net/qq_36010886/article/details/130471425
為什么需要多線程
? ? ? ? 為了更快的響應,同時合理的利用CPU資源目派。
? ? ? ? 但是數(shù)據(jù)傳輸過程中都經(jīng)過CPU寇仓、內(nèi)存蜈出、I/O 設備等,這些速度都有極大的差異,如何均衡計算機體系結構、操作系統(tǒng)、編譯程序都做出了優(yōu)化翰守。
CPU 增加了緩存孵奶,以均衡與內(nèi)存的速度差異;// 導致 可見性問題
操作系統(tǒng)增加了進程蜡峰、線程了袁,以分時復用 CPU,進而均衡 CPU 與 I/O 設備的速度差異湿颅;// 導致 原子性問題
編譯程序優(yōu)化指令執(zhí)行次序载绿,使得緩存能夠得到更加合理地利用。// 導致 有序性問題
并發(fā)產(chǎn)生問題的根源
可見性
一個線程對共享變量的修改油航,另外一個線程能夠立刻看到崭庸。
原子性
即一個操作或者多個操作,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷谊囚,要么就都不執(zhí)行怕享。
有序性
程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在執(zhí)行程序時為了提高性能镰踏,編譯器和處理器常常會對指令做重排序函筋。重排序分三種類型:
編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下奠伪,可以重新安排語句的執(zhí)行順序跌帐。
指令級并行的重排序∈仔福現(xiàn)代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行谨敛。如果不存在數(shù)據(jù)依賴性究履,處理器可以改變語句對應機器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)的重排序佣盒。由于處理器使用緩存和讀 / 寫緩沖區(qū)挎袜,這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
從 java 源代碼到最終實際執(zhí)行的指令序列肥惭,會分別經(jīng)歷下面三種重排序:
源代碼-》編譯器重排序-》指令級重排序-》內(nèi)存重排序-》最終執(zhí)行的指令序列
? ? ? 這些重排序都可能會導致多線程出現(xiàn)內(nèi)存可見性問題盯仪,對于編譯器,JVM有提供內(nèi)存屏蔽指令(volatile關鍵字)來禁止特定類型的編譯器重排序和處理器重排序蜜葱。
處理器重排序與內(nèi)存屏障指令
? ? ? 為了保證內(nèi)存可見性全景,java 編譯器在生成指令序列的適當位置會插入內(nèi)存屏障指令來禁止特定類型的編譯器重排序和處理器重排序。不同硬件實現(xiàn)內(nèi)存屏障的方式不同牵囤,Java內(nèi)存屏障主要有Load和Store兩類爸黄。JMM 把內(nèi)存屏障指令分為下列四類:
屏障類型指令示例說明
LoadLoad BarriersLoad1; LoadLoad; Load2確保 Load1 數(shù)據(jù)的裝載,之前于 Load2 及所有后續(xù)裝載指令的裝載揭鳞。
StoreStore BarriersStore1; StoreStore; Store2確保 Store1 數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)炕贵,之前于 Store2 及所有后續(xù)存儲指令的存儲。
LoadStore BarriersLoad1; LoadStore; Store2確保 Load1 數(shù)據(jù)裝載野崇,之前于 Store2 及所有后續(xù)的存儲指令刷新到內(nèi)存称开。
StoreLoad BarriersStore1; StoreLoad; Load2確保 Store1 數(shù)據(jù)對其他處理器變得可見(指刷新到內(nèi)存),之前于 Load2 及所有后續(xù)裝載指令的裝載乓梨。執(zhí)行該屏障開銷會很昂貴鳖轰,因為當前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中。
? ? ? 另外也可以通過Volatile關鍵字來實現(xiàn)屏障指令效果扶镀。
硬件層的內(nèi)存屏障
? ? ? Intel硬件提供了一系列的內(nèi)存屏障蕴侣,主要有:
1、lfence臭觉,是一種Load Barrier 讀屏障2昆雀、sfence, 是一種Store Barrier 寫屏障3、mfence, 是一種全能型的屏障蝠筑,具備ifence和sfence的能力4忆肾、Lock前綴,Lock不是一種內(nèi)存屏障菱肖,但是它能完成類似內(nèi)存屏障的功能客冈。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖稳强。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令场仲。
怎么解決并發(fā)問題-JMM(Java內(nèi)存模型)
? ? ? ? Java 內(nèi)存模型規(guī)范了 JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法和悦。分別是:volatile(可見性、有序性)渠缕、synchronized (原子性鸽素、可見性)和 final 三個關鍵字和Happens-Before 規(guī)則。
? ? ? 在并發(fā)編程中亦鳞,線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞馍忽。在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài)燕差,線程之間通過寫 - 讀內(nèi)存中的公共狀態(tài)來隱式進行通信遭笋。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài)徒探,線程之間必須通過明確的發(fā)送消息來顯式進行通信瓦呼。JMM 通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來保證內(nèi)存可見性测暗。
? ? ? ? 在 java 中央串,所有實例域、靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中碗啄,堆內(nèi)存在線程之間共享质和。局部變量,方法定義參數(shù)和異常處理器參數(shù)在當前線程的棧中稚字,不會在線程之間共享饲宿,它們不會有內(nèi)存可見性問題,也不受內(nèi)存模型的影響尉共。
三個關鍵字
volatile
? ? ? 當一個共享變量被volatile修飾時褒傅,它會保證修改的值會立即被更新到主存弃锐,當有其他線程需要讀取時袄友,它會去內(nèi)存中讀取新值,以此來保證可見性霹菊。volatile主要用來防重排序剧蚣。
? ? ? volatile不能保證完全的原子性,只能保證單次的讀/寫操作具有原子性旋廷。
? ? ? volatile 變量的內(nèi)存可見性是基于內(nèi)存屏障(Memory Barrier)實現(xiàn)鸠按。volatile 寫是在前面和后面分別插入內(nèi)存屏障,而 volatile 讀操作是在后面插入兩個內(nèi)存屏障饶碘。
內(nèi)存屏障說明
StoreStore 屏障禁止上面的普通寫和下面的 volatile 寫重排序目尖。
StoreLoad 屏障防止上面的 volatile 寫與下面可能有的 volatile 讀/寫重排序。
LoadLoad 屏障禁止下面所有的普通讀操作和上面的 volatile 讀重排序扎运。
LoadStore 屏障禁止下面所有的普通寫操作和上面的 volatile 讀重排序瑟曲。
? ? ? 內(nèi)存屏障饮戳,又稱內(nèi)存柵欄,是一個 CPU 指令洞拨。在程序運行時扯罐,為了提高執(zhí)行性能,編譯器和處理器會對指令進行重排序烦衣,JMM 為了保證在不同的編譯器和 CPU 上有相同的結果歹河,通過插入特定類型的內(nèi)存屏障來禁止+ 特定類型的編譯器重排序和處理器重排序,插入一條內(nèi)存屏障會告訴編譯器和 CPU:不管什么指令都不能和這條 Memory Barrier 指令重排序花吟。
? ? ? 為了保證各個處理器的緩存是一致的秸歧,實現(xiàn)了緩存一致性協(xié)議(MESI),每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了示辈,當處理器發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址被修改寥茫,就會將當前處理器的緩存行設置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候矾麻,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里纱耻。
? ? ? 所有多核處理器下還會完成:當處理器發(fā)現(xiàn)本地緩存失效后,就會從內(nèi)存中重讀該變量數(shù)據(jù)险耀,即可以獲取當前最新值弄喘。
? ? ? ? 緩存是分段(line)的,一個段對應一塊存儲空間甩牺,稱之為緩存行蘑志,它是 CPU 緩存中可分配的最小存儲單元,大小 32 字節(jié)贬派、64 字節(jié)急但、128 字節(jié)不等,這與 CPU 架構有關搞乏,通常來說是 64 字節(jié)波桩。
? ? ? ? LOCK# 因為鎖總線效率太低,因此使用了多組緩存请敦。 為了使其行為看起來如同一組緩存那樣镐躲。因而設計了緩存一致性協(xié)議。 緩存一致性協(xié)議有多種侍筛,但是日常處理的大多數(shù)計算機設備都屬于 " 嗅探(snooping)" 協(xié)議萤皂。
? ? ? ? 所有內(nèi)存的傳輸都發(fā)生在一條共享的總線上,而所有的處理器都能看到這條總線匣椰。 緩存本身是獨立的裆熙,但是內(nèi)存是共享資源,所有的內(nèi)存訪問都要經(jīng)過仲裁(同一個指令周期中,只有一個 CPU 緩存可以讀寫內(nèi)存)入录。
? ? ? ? CPU 緩存不僅僅在做內(nèi)存?zhèn)鬏數(shù)臅r候才與總線打交道齐媒,而是不停在嗅探總線上發(fā)生的數(shù)據(jù)交換,跟蹤其他緩存在做什么纷跛。 當一個緩存代表它所屬的處理器去讀寫內(nèi)存時喻括,其它處理器都會得到通知,它們以此來使自己的緩存保持同步贫奠。 只要某個處理器寫內(nèi)存唬血,其它處理器馬上知道這塊內(nèi)存在它們的緩存段中已經(jīng)失效。
? ? ? ? 如果對聲明了 volatile 的變量進行寫操作唤崭,JVM 就會向處理器發(fā)送一條 lock 前綴的指令拷恨,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
synchronized
? ? ? ? Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作谢肾,如果要實現(xiàn)更大范圍操作的原子性腕侄,可以通過synchronized和Lock來實現(xiàn)。由于synchronized和Lock通過加鎖能夠保證任一時刻只有一個線程執(zhí)行該代碼塊芦疏,并且在釋放鎖之前會將對變量的修改刷新到主存當中冕杠,從而保證了原子性、可見性酸茴、有序性分预。
final
1)基礎使用
? ? ? 修飾類。被修飾的類不能被繼承薪捍。當遇到數(shù)據(jù)結構需要當前類笼痹,可以采用開發(fā)原則組合復用原則。
? ? ? 修飾方法酪穿。不能被重寫凳干,但是可以被重載,private 方法是隱式的final被济。
? ? ? 修飾參數(shù)救赐。無法在方法中更改參數(shù)引用所指向的對象。這個特性主要用來向匿名內(nèi)部類傳遞數(shù)據(jù)溉潭。
? ? ? 修飾變量净响。修飾的變量不可變少欺。Java允許生成空白final喳瓣,也就是說被聲明為final但又沒有給出定值的字段,但是必須在該字段被使用之前被賦值。必須在構造函數(shù)退出前設置它的值赞别。
2)final域重排序規(guī)則
? ? ? 按照final修飾的數(shù)據(jù)類型分類:
基本數(shù)據(jù)類型:
final域寫:禁止final域寫與構造方法重排序畏陕,即禁止final域寫重排序到構造方法之外,從而保證該對象對所有線程可見時仿滔,該對象的final域全部已經(jīng)初始化過惠毁。
final域讀:禁止初次讀對象的引用與讀該對象包含的final域的重排序犹芹。
引用數(shù)據(jù)類型:
額外增加約束:禁止在構造函數(shù)對一個final修飾的對象的成員域的寫入與隨后將這個被構造的對象的引用賦值給引用變量 重排序
3)final的實現(xiàn)原理
? ? ? final域的重排序也是在指定位置插入屏障指令。寫final域會要求編譯器在final域寫之后鞠绰,構造函數(shù)返回前插入一個StoreStore屏障腰埂。讀final域的重排序規(guī)則會要求編譯器在讀final域的操作前插入一個LoadLoad屏障。
Java內(nèi)存模型(JMM)
? ? ? ? JMM遵循一個基本原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序)蜈膨,編譯器和處理器怎么優(yōu)化都行屿笼。
Happens-Before原則
? ? ? ? 一個 happens-before 規(guī)則通常對應于多個編譯器重排序規(guī)則和處理器重排序規(guī)則。
單一線程原則翁巍。單一線程內(nèi)驴一,代碼順序決定執(zhí)行順序。
管程鎖定規(guī)則灶壶。對于同一個鎖肝断,解鎖(UnLock)總是發(fā)生在加鎖之前(Lock)。
volatile 變量規(guī)則驰凛。對于同一個Volatile變量**胸懈,寫操作總是發(fā)生在讀操作之前。
線程啟動規(guī)則恰响。一個線程的 start()操作箫荡,總是發(fā)生在這個線程所有動作之前。
線程加入規(guī)則渔隶。Thread對象的結束先行發(fā)生于 join() 方法返回羔挡。
線程中斷規(guī)則。對線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生间唉,可以通過 interrupted() 方法檢測到是否有中斷發(fā)生绞灼。
對象終結規(guī)則。一個對象的初始化操作總是發(fā)生在它的finalize方法之前呈野。
傳遞性低矮。如果A先于B,B先于C被冒,那么A先于C军掂。
JMM 把 happens- before 要求禁止的重排序兩類:
會改變程序執(zhí)行結果的重排序。
不會改變程序執(zhí)行結果的重排序昨悼。
JMM 對這兩種不同性質的重排序蝗锥,采取了不同的策略:
對于會改變程序執(zhí)行結果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序率触。
對于不會改變程序執(zhí)行結果的重排序终议,JMM 對編譯器和處理器不作要求(JMM 允許這種重排序)。
鎖優(yōu)化
? ? ? 詳見JVM中的鎖Synchronized鎖優(yōu)化
常見線程安全的實現(xiàn)方案
一、互斥同步
? ? ? ? 互斥同步最主要的問題就是線程阻塞和喚醒所帶來的性能問題穴张,因此這種同步也稱為阻塞同步细燎。互斥同步屬于一種悲觀的并發(fā)策略,總是認為只要不去做正確的同步措施皂甘,那就肯定會出現(xiàn)問題玻驻。
Synchronized
1)Synchronized的使用
? ? ? 1-1、對象鎖:代碼塊形式偿枕,包含方法鎖(默認鎖對象為this,當前實例對象)和同步代碼塊鎖(自己指定鎖對象)
? ? ? 1-2击狮、方法鎖:synchronized修飾普通方法,鎖對象默認為this
? ? ? 1-3益老、類鎖:synchronize修飾靜態(tài)的方法或指定鎖對象為Class對象
2)Synchronized原理分析
? ? 2-1) 加鎖和釋放鎖的原理
? ? ? ? 任意線程對Object的訪問彪蓬,首先要獲得Object的監(jiān)視器,如果獲取失敗捺萌,該線程就進入同步狀態(tài)档冬,線程狀態(tài)變?yōu)锽LOCKED(阻塞),當Object的監(jiān)視器占有者釋放后桃纯,在同步隊列中得線程就會有機會重新獲取該監(jiān)視器酷誓。
? ? ? ? Monitorenter和Monitorexit指令,會讓對象在執(zhí)行态坦,使其鎖計數(shù)器加1或者減1盐数。每一個對象在同一時間只與一個monitor(鎖)相關聯(lián),而一個monitor在同一時間只能被一個線程獲得伞梯。monitor計數(shù)器為0玫氢,代表目前沒有被獲得。
? ? 2-2)可重入原理:加鎖次數(shù)計數(shù)器
? ? ? 可重入鎖:又名遞歸鎖谜诫,是指在同一個線程在外層方法獲取鎖的時候漾峡,再進入該線程的內(nèi)層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經(jīng)獲取過還沒釋放而阻塞喻旷。
? ? ? ? 即在同一鎖程中生逸,每個對象擁有一個monitor計數(shù)器,當線程獲取該對象鎖后且预,monitor計數(shù)器就會加一槽袄,釋放鎖后就會將monitor計數(shù)器減一,線程不需要再次獲取同一把鎖锋谐。
? ? 2-3)保證可見性的原理:內(nèi)存模型和happens-before規(guī)則
? ? ? ? Synchronized的happens-before規(guī)則遍尺,即監(jiān)視器鎖規(guī)則:對同一個監(jiān)視器的解鎖,happens-before于對該監(jiān)視器的加鎖怀估。
3)JVM中鎖的優(yōu)化
? ? ? ? JVM中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的狮鸭,但是由于使用Mutex Lock需要將當前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來執(zhí)行,這種切換的代價是非常昂貴的多搀;然而在現(xiàn)實中的大部分情況下歧蕉,同步方法是運行在單線程環(huán)境如果每次都調(diào)用Mutex Lock那么將嚴重的影響程序的性能。
? ? ? ? 在jdk1.6中對鎖的實現(xiàn)引入了大量的優(yōu)化康铭,如鎖粗化(Lock Coarsening)厚骗、鎖消除(Lock Elimination)缩膝、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷叔汁。
鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作蜒什,將多個連續(xù)的鎖擴展成一個范圍更大的鎖拇泛。
鎖消除(Lock Elimination):通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數(shù)據(jù)的鎖保護。通過逃逸分析也可以在線程本的Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)悯搔。
輕量級鎖(Lightweight Locking):在無鎖競爭的情況下避免調(diào)用操作系統(tǒng)層面的重量級互斥鎖骑丸,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下妒貌,執(zhí)行CAS指令失敗的線程將調(diào)用操作系統(tǒng)互斥鎖進入到阻塞狀態(tài)通危,當鎖被釋放的時候被喚醒(具體處理步驟下面詳細討論)。
偏向鎖(Biased Locking):當一個線程訪問同步塊并獲取鎖時灌曙,會在對象頭的Mark Word和棧幀中的鎖記錄里存儲鎖偏向的線程ID菊碟,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖。只需要驗證是否指向當前線程的偏向鎖在刺。如果成功逆害,表示線程已經(jīng)獲取到了鎖。偏向鎖使用了一種等待競爭出現(xiàn)才會釋放鎖的機制蚣驼,假如出現(xiàn)了競爭忍燥,會等當前持有鎖的對象執(zhí)行完畢,直接將對象頭設置為無鎖狀態(tài)隙姿,若此時有多個線程競爭梅垄,就膨脹為重量級鎖;若只有一個線程搶奪输玷,則是輕量級鎖队丝。
適應性自旋(Adaptive Spinning):當線程在獲取輕量級鎖的過程中執(zhí)行CAS操作失敗時,在進入與monitor相關聯(lián)的操作系統(tǒng)重量級鎖(mutex semaphore)前會進入忙等待(Spinning)然后再次嘗試欲鹏,當嘗試一定的次數(shù)后如果仍然沒有成功則調(diào)用與該monitor關聯(lián)的semaphore(即互斥鎖)進入到阻塞狀態(tài)机久。
自旋鎖:在JDK1.4 中引入了,在JDK 1.6后默認為開啟狀態(tài)赔嚎。在JDK定義中膘盖,自旋鎖默認的自旋次數(shù)為10次胧弛,用戶可以使用參數(shù)-XX:PreBlockSpin來更改。
? ? ? ? CAS是CPU的一條指令侠畔,其具有原子性结缚,原子性是由CPU硬件層面保證的。內(nèi)存位置(V)软棺、預期原值(A)红竭、新值(B)。若內(nèi)存位置與預期原值匹配則處理器將該位置更新為新值喘落。否則不做操作茵宪。無論何種情況都會在CAS指令之前返回該位置值。這個過程是原子性的瘦棋。
? ? ? ? Synchronized一共有四種狀態(tài):無鎖稀火、偏向鎖、輕量級鎖赌朋、重量級鎖憾股,它會隨著競爭情況逐漸升級。鎖可以升級但是不可以降級箕慧,目的是為了提供獲取鎖和釋放鎖的效率服球。
? ? ? ? 鎖膨脹方向: 無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖 (此過程是不可逆的)
? ? ? ? 輕量級鎖和偏向鎖的區(qū)分,在Java對象頭中(Object Header)存在兩部分颠焦。第一部分用于存儲對象自身的運行時數(shù)據(jù)斩熊,HashCode、GC Age伐庭、鎖標記位粉渠、是否為偏向鎖。等圾另。一般為32位或者64位(視操作系統(tǒng)位數(shù)定)霸株。官方稱之為Mark Word,它是實現(xiàn)輕量級鎖和偏向鎖的關鍵集乔。 另外一部分存儲的是指向方法區(qū)對象類型數(shù)據(jù)的指針(Klass Point)去件,如果對象是數(shù)組的話,還會有一個額外的部分用于存儲數(shù)據(jù)的長度扰路。
? ? ? 在線程執(zhí)行同步塊之前尤溜,JVM會先在當前線程的棧幀中創(chuàng)建一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(JVM會將對象頭中的Mark Word拷貝到鎖記錄中汗唱。
? ? ? ? 如果當前對象沒有被鎖定宫莱,那么鎖標志位為01狀態(tài)。
? ? ? ? JVM使用CAS操作將標記字段Mark Word拷貝到鎖記錄中哩罪,并且將Mark Word更新為指向Lock Record的指針授霸。
? ? ? ? 如果更新成功了巡验,那么這個線程就擁用了該對象的鎖,并且對象Mark Word的鎖標志位更新為(Mark Word中最后的2bit)00碘耳,即表示此對象處于輕量級鎖定狀態(tài)显设。
? ? ? ? 如果這個更新操作失敗,JVM會檢查當前的Mark Word中是否存在指向當前線程的棧幀的指針藏畅,如果有敷硅,說明該鎖已經(jīng)被獲取功咒,可以直接調(diào)用愉阎,是偏向鎖。如果沒有力奋,則說明該鎖被其他線程搶占了榜旦,如果有兩條以上的線程競爭同一個鎖,直接膨脹為重量級鎖景殷,沒有獲得鎖的線程會被阻塞溅呢。鎖的標志位為10.Mark Word中存儲的指向重量級鎖的指針。
ReentrantLocked
? ? ? ? 多線程競爭一個鎖時猿挚,其余未得到鎖的線程只能不停的嘗試獲得鎖咐旧,而不能中斷。高并發(fā)的情況下會導致性能下降绩蜻。ReentrantLock的lockInterruptibly()方法可以優(yōu)先考慮響應中斷铣墨。 一個線程等待時間過長,它可以中斷自己办绝。有了這個機制伊约,使用ReentrantLock時就不會像synchronized那樣產(chǎn)生死鎖了。
? ? ? ? ReentrantLock實現(xiàn)了Lock接口孕蝉,Lock接口中定義了lock與unlock相關操作屡律,并且還存在newCondition方法,表示生成一個條件降淮。 ReentrantLock類內(nèi)部總共存在Sync超埋、NonfairSync、FairSync三個類佳鳖,NonfairSync與FairSync類繼承自Sync類纳本,Sync類繼承自AbstractQueuedSynchronizer抽象類。
Lock類的4個方法:
lock(): 加鎖
unlock(): 解鎖
tryLock(): 嘗試獲取鎖腋颠,返回一個boolean值
tryLock(long,TimeUtil): 嘗試獲取鎖繁成,可以設置超時
1)Sync類存在如下方法和作用如下。
2)NonfairSync類繼承了Sync類淑玫,表示采用非公平策略獲取鎖巾腕,其實現(xiàn)了Sync類中抽象的lock方法面睛。
3)FairSync類也繼承了Sync類,表示采用公平策略獲取鎖尊搬,其實現(xiàn)了Sync類中的抽象lock方法叁鉴。跟蹤lock方法的源碼可知,當資源空閑時佛寿,它總是會先判斷sync隊列(AbstractQueuedSynchronizer中的數(shù)據(jù)結構)是否有等待時間更長的線程幌墓,如果存在,則將該線程加入到等待隊列的尾部冀泻,實現(xiàn)了公平獲取原則常侣。
? ? ? 分析ReentrantLock的源碼,可知對其操作都轉化為對Sync對象的操作弹渔,由于Sync繼承了AQS胳施,所以基本上都可以轉化為對AQS的操作。如將ReentrantLock的lock函數(shù)轉化為對Sync的lock函數(shù)的調(diào)用肢专,而具體會根據(jù)采用的策略(如公平策略或者非公平策略)的不同而調(diào)用到Sync的不同子類舞肆。
二、非阻塞同步
? ? ? 基于沖突檢測的樂觀并發(fā)策略: 先進行操作博杖,如果沒有其它線程爭用共享數(shù)據(jù)椿胯,那操作就成功了,否則采取補償措施(不斷地重試剃根,直到成功為止)哩盲。這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要將線程阻塞,因此這種同步操作稱為非阻塞同步跟继。
CAS
? ? ? ? 樂觀鎖需要操作和沖突檢測這兩個步驟具備原子性种冬,只能靠硬件來完成。硬件支持的原子性操作最典型的是: 比較并交換(Compare-and-Swap舔糖,CAS)娱两。CAS 指令需要有 3 個操作數(shù),分別是內(nèi)存地址 V金吗、舊的預期值 A 和新值 B十兢。當執(zhí)行操作時,只有當 V 的值等于 A摇庙,才將 V 的值更新為 B旱物。
Atomic類
? ? ? J.U.C 包里面的整數(shù)原子類 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 類的 CAS 操作卫袒。
ABA
? ? ? 如果一個變量初次讀取的時候是 A 值宵呛,它的值被改成了 B,后來又被改回為 A夕凝,那 CAS 操作就會誤認為它從來沒有被改變過宝穗。J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題户秤,它可以通過控制變量值的版本來保證 CAS 的正確性。 但ABA存在的問題逮矛,改用傳統(tǒng)的互斥同步可能會比原子類更高效鸡号。
三、無同步方案
? ? ? 要保證線程安全须鼎,并不是一定就要進行同步鲸伴。如果一個方法本來就不涉及共享數(shù)據(jù),那它自然就無須任何同步措施去保證正確性晋控。例如:鎖消除汞窗。
棧封閉
? ? ? 多個線程訪問同一個方法的局部變量時,不會出現(xiàn)線程安全問題糖荒,因為局部變量存儲在虛擬機棧中杉辙,屬于線程私有的模捂。
線程本地存儲
? ? ? 如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享捶朵,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行。如果能保證狂男,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi)综看,這樣,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題岖食。
? ? ? 在一些場景 (尤其是使用線程池) 下红碑,由于 ThreadLocal.ThreadLocalMap 的底層數(shù)據(jù)結構導致 ThreadLocal 有內(nèi)存泄漏的情況,應該盡可能在每次使用 ThreadLocal 后手動調(diào)用 remove()泡垃,以避免出現(xiàn) ThreadLocal 經(jīng)典的內(nèi)存泄漏甚至是造成自身業(yè)務混亂的風險析珊。
可重入代碼
? ? ? ? 可重入代碼也叫做純代碼(Pure Code),可以在代碼執(zhí)行的任何時刻中斷它蔑穴,轉而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身)忠寻,而在控制權返回后,原來的程序不會出現(xiàn)任何錯誤存和。
? ? ? ? 可重入代碼有一些共同的特征奕剃,例如不依賴存儲在堆上的數(shù)據(jù)和公用的系統(tǒng)。