鎖是最常用的同步方法之一笛臣。在高并發(fā)的環(huán)境下沈堡,激烈的鎖競爭會(huì)導(dǎo)致程序的性能下降诞丽。
對于單任務(wù)或者單線程的應(yīng)用而言僧免,其主要資源消耗都花在任務(wù)本身懂衩,它既不需要維護(hù)并行數(shù)據(jù)結(jié)構(gòu)間的一致性狀態(tài)勃痴,也不需要為線程的切換和調(diào)度花費(fèi)時(shí)間沛申。對于多線程應(yīng)用來說铁材,系統(tǒng)除了處理功能需求外著觉,還需要額外維護(hù)多線程環(huán)境的特有信息饼丘,如線程本身的元數(shù)據(jù)、線程的調(diào)度辽话、線程上下文的切換等肄鸽。并行計(jì)算之所以能提高系統(tǒng)的性能,并不是因?yàn)樗?少干活"了油啤,而是因?yàn)椴⑿杏?jì)算可以更合理地進(jìn)行任務(wù)調(diào)度典徘,充分利用各個(gè)CPU資源。
如何提高鎖性能
減少鎖持有時(shí)間
對于使用鎖進(jìn)行并發(fā)控制的應(yīng)用程序而言益咬,在鎖競爭過程中逮诲,單個(gè)線程對鎖的持有時(shí)間與系統(tǒng)性能有著直接的關(guān)系。如果鎖的持有鎖時(shí)間越長,那么鎖的競爭程度也就越激烈梅鹦。
簡單來講就是:要100個(gè)人填寫信息表淑掌,但是只有一根筆担敌,每個(gè)人如果沒想好怎么填,那么每個(gè)人持有筆的時(shí)間就會(huì)很長攒暇,那么總的時(shí)間就會(huì)變長。
因此減少對某個(gè)鎖的持有時(shí)間,以減少線程間互斥。例如下面這段代碼:
public synchronized void synMethod(){ method1(); mainMethod(); method2();}
上面那段代碼中,只有mainMethod()方法需要做同步控制魄宏,而method1()和method2()不需要做同步控制,那么上面那段在高并發(fā)的情況下對整個(gè)方法都進(jìn)行了同步控制,如果method1()和method2()兩個(gè)方法的耗時(shí)長,那么會(huì)導(dǎo)致整個(gè)程序的執(zhí)行時(shí)間變長。因此我們可以選擇下面這樣優(yōu)化:
public void synMethod(){ method1(); synchronized(this){ mainMethod(); } method2();}
這樣做的好處就是,只針對mainMethod()方法做了同步控制勒极,鎖占用的時(shí)間相對較短,因此能夠有較高的并發(fā)度散休。較少鎖的持有時(shí)間有助于降低鎖沖突的可能性限府,進(jìn)而提升系統(tǒng)的并發(fā)能力署穗。
減小鎖粒度
減小鎖粒度也是一種削弱多線程鎖競爭的有效手段诺舔。這種技術(shù)典型的使用場景就是ConcurrentHashMap類的實(shí)現(xiàn)逸嘀。對ConcurrentHashMap有所了解的小伙伴應(yīng)該知道,傳統(tǒng)的HashTable之所以是線程安全的就是因?yàn)樗菍φ麄€(gè)方法加鎖。而ConcurrentHashMap的性能比較高是因?yàn)樗鼉?nèi)部細(xì)分了若干個(gè)小的HashMap,稱之為段(SEGMENT)市袖。在默認(rèn)情況下微峰,一個(gè)ConcurrentHashMap類可以細(xì)分為16個(gè)端谋币,性能相當(dāng)于提升了16倍精肃。
在ConcurrentHashMap中增加一個(gè)數(shù)據(jù)资溃,并不是對整個(gè)HashMap加鎖拱绑,而是首先根據(jù)hashcode得出應(yīng)該被存放在哪個(gè)段中类腮,然后對該段加鎖,并完成put()操作昭殉。當(dāng)多個(gè)線程進(jìn)行put()操作的時(shí)候,如果鎖的不是同一個(gè)段越走,那么就可以實(shí)現(xiàn)并行操作骡澈。
但是,減小鎖粒度會(huì)帶來一個(gè)新的問題:當(dāng)系統(tǒng)需要取得全局鎖時(shí),其消耗的資源會(huì)比較多赤炒。例如:當(dāng)ConcurrentHashMap調(diào)用size()方法時(shí)雪情,需要或者所有子段的鎖正卧。雖然事實(shí)上窘行,size()方法會(huì)先使用無鎖的方式求和,如果失敗才會(huì)嘗試這種方式,但是在高并發(fā)的情況下逛艰,ConcurrentHashMap的性能依然要弱于同步的HashMap躏碳。
減小鎖粒度,就是指縮小鎖定對象的范圍瓮孙,從而降低鎖沖突的可能性唐断,進(jìn)而提高系統(tǒng)的并發(fā)能力
用讀寫鎖來替換獨(dú)占鎖
讀寫分離鎖可以有效地幫助減少鎖競爭选脊,提高系統(tǒng)性能。比如:A1脸甘、A2恳啥、A3三個(gè)線程進(jìn)行寫操作,B1丹诀、B2钝的、B3三個(gè)線程進(jìn)行讀操作,如果使用重入鎖或者內(nèi)部鎖铆遭,那么所有讀之間硝桩,讀與寫之間,寫之間都是串行操作枚荣。但是因?yàn)樽x操作并不會(huì)造成數(shù)據(jù)的完整性破壞碗脊,因此這種等待是不合理的。
因此可以使用讀寫分離鎖ReadWriteLock來提高系統(tǒng)的性能橄妆。使用示例如下:
鎖粗化
通常情況下矢劲,為了保證多線程間的有效并發(fā),會(huì)要求每個(gè)線程持有鎖的時(shí)間盡量短慌随,在使用完公共資源后芬沉,應(yīng)該立即釋放鎖,只有這樣阁猜,等待在這個(gè)鎖上的其他線程才能盡早地獲得資源執(zhí)行任務(wù)丸逸。
錯(cuò)誤示例:
public void synMethod(){ synchronized(this){ method1(); } synchronized(this){ method2(); }}
優(yōu)化后:
public void synMethod(){ synchronized(this){ method1(); method2(); }}
尤其是在循環(huán)中要注意鎖的粗化
錯(cuò)誤示例:
public void synMethod(){ for (int i = 1; i < n; i++) { synchronized(lock){ //do sth ... } }}
優(yōu)化后:
synchronized(lock){ for (int i = 1; i < n; i++) { //do sth ... }}
JVM進(jìn)行的鎖優(yōu)化
偏向鎖
鎖偏向是一種針對加鎖操作的優(yōu)化手段。核心思想:如果一個(gè)線程獲得了一個(gè)鎖笛园,那么這個(gè)鎖就進(jìn)入了偏向模式隘击,當(dāng)這個(gè)線程釋放完這個(gè)鎖后,下次同其他線程再次請求時(shí)研铆,無須在做任何同步操作埋同。這樣就節(jié)省了大量的鎖申請相關(guān)操作。
但是在鎖競爭比較激烈的場合棵红,效果不佳凶赁,因?yàn)樵诟偁幖ち业膱龊希钣锌赡艿那闆r就是每次都是不同的線程來請求,這樣偏向模式會(huì)失效虱肄,因此還不如不啟用偏向鎖致板。可以通過 JVM參數(shù) -XX:+UseBiasedLocking開啟偏向鎖咏窿。
輕量級鎖
如果偏向鎖失敗斟或,那么虛擬機(jī)并不會(huì)立即掛起線程,它還會(huì)使用一種稱為輕量級鎖的優(yōu)化手段集嵌。輕量級鎖的操作也很方便萝挤,它只是簡單地將對象頭部作為指針指向持有鎖的線程堆棧的頭部,來判斷一個(gè)線程是否持有對象鎖根欧。如果線程獲得輕量級鎖成功怜珍,則可以順利進(jìn)入臨界區(qū),如果輕量級鎖加鎖失敗凤粗,則表示其他線程搶先爭奪到了鎖酥泛,那么當(dāng)前線程的鎖請求就會(huì)膨脹為重量級鎖。
自旋鎖
鎖膨脹后侈沪,為了避免線程真實(shí)地在操作系統(tǒng)層面掛起揭璃,虛擬機(jī)還會(huì)做最后的努力——自旋鎖晚凿。當(dāng)前線程暫時(shí)獲取不到鎖亭罪,但是如果簡單粗暴地將這個(gè)線程掛起是一種得不償失的操作,因此虛擬機(jī)會(huì)讓當(dāng)前線程做幾個(gè)空循環(huán)歼秽,在經(jīng)過若干次循環(huán)后应役,如果可以得到鎖,那么就順利進(jìn)入臨界區(qū)燥筷。
重量級鎖
如果經(jīng)過自旋還不能獲得鎖箩祥,才會(huì)真的將線程在操作系統(tǒng)層面掛起,升級為 重量級鎖 **
鎖消除
Java虛擬機(jī)在 JIT 編譯時(shí)肆氓,會(huì)通過對運(yùn)行上下文進(jìn)行掃描袍祖,去除不可能存在共享資源競爭的鎖,通過鎖消除谢揪,可以節(jié)省毫無意義的請求鎖時(shí)間蕉陋。
public String[] createArrays() { Vector<Integer> vector = new Vector<>(); for (int i = 1; i < 100; i++) { vector.add(i); } return vector.toArray(new String[]{});}
上面一段代碼中,因?yàn)?vector 這個(gè)變量是定義在createArrays()這個(gè)方法中拨扶,是一個(gè)局部變量凳鬓,在線程棧中分配的,屬于線程私有的數(shù)據(jù)患民,因此不存在資源競爭的情況缩举。而Vector內(nèi)部所有加鎖同步都是沒有必要的,如果虛擬機(jī)檢測到這種情況,就會(huì)將這些無用的鎖操作去除仅孩。
鎖消除涉及的一項(xiàng)關(guān)鍵技術(shù)為逃逸分析托猩,所謂逃逸分析就是觀察某一個(gè)變量是否會(huì)逃出某一個(gè)作用域。在上面例子中辽慕,變量vector 沒有逃出createArrays()這個(gè)函數(shù)的方位站刑,因此虛擬機(jī)才會(huì)就將這個(gè)變量的加鎖操作去除。如果 createArrays()返回的不是 String數(shù)組鼻百,而是 vector 本身绞旅,那么就認(rèn)為變量 vector 逃出了當(dāng)前函數(shù),會(huì)被其他線程所訪問到温艇。例如下面代碼:
public Vector<Integer> createList() { Vector<Integer> vector = new Vector<>(); for (int i = 1; i < 100; i++) { vector.add(i); } return vector;}
ThreadLocal
除了控制資源的訪問外因悲,我們還可以通過增加資源來保證所有對象的線程安全。簡單來講就是:要100個(gè)人填寫信息表勺爱,我們可以分配100根筆給他們填寫晃琳,人手一根,那么填寫的速度也將大大增加琐鲁。
上面這個(gè)代碼顾翼,如果沒有同步控制則會(huì)出現(xiàn)java.lang.NumberFormatException: multiple points和java.lang.NumberFormatException: For input string: ""異常,因?yàn)镾impleDateFormat不是線程安全的奈泪,除非加鎖控制适贸。但是除了加鎖我們還有沒有其他方法呢,答案是有的涝桅,那就是使用ThreadLocal拜姿,每個(gè)線程分配一個(gè)SimpleDateFormat。
為每一個(gè)線程分配不同的對象蛤肌,需要在應(yīng)用層面保證 ThreadLocal 只起到了簡單的容器作用
ThreadLocal的實(shí)現(xiàn)原理
set()方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}
先獲取當(dāng)前線程對象壁却,然后通過getMap()方法拿到線程的ThreadLocalMap,并將值存入ThreadLocalMap中⊙岸ǎ可以簡單把ThreadLocalMap理解為一個(gè)Map儒洛,其中key為當(dāng)前線程對象,value便是我們所需要的值狼速。
get()方法:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}
先獲取到當(dāng)前線程的ThreadLocalMap琅锻,然后通過將自己作為key取得內(nèi)部的實(shí)際數(shù)據(jù)
如果希望及時(shí)回收對象,我們應(yīng)該使用ThreadLocal.remove()方法將這個(gè)變量移除,否則如果將一些大的對象設(shè)置到 ThreadLocal中恼蓬,沒有及時(shí)回收惊完,會(huì)造成內(nèi)存泄漏的可能。
無鎖
鎖分為樂觀鎖和悲觀鎖处硬,而無鎖就是一種樂觀的策略小槐,它是使用一種叫比較并交換(CAS,Compare And Swap)的技術(shù)來鑒別線程沖突荷辕,一旦檢測到?jīng)_突發(fā)生凿跳,就重試當(dāng)前操作直到?jīng)]有沖突為止。
比較并交換
CAS的算法過程是:包含三個(gè)參數(shù) CAS(V,E,N)疮方,其中V表示要更新的變量控嗜,E表示預(yù)期值,N表示新值骡显。僅當(dāng)V值等于E值時(shí)疆栏,才會(huì)將V值設(shè)置為N值。最后返回當(dāng)前V的真實(shí)值惫谤。當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí)壁顶,只有一個(gè)會(huì)勝出,并成功更新溜歪,其他均會(huì)失敗若专。失敗的線程不會(huì)被掛起,僅是被告知失敗痹愚,并且允許再次嘗試富岳,當(dāng)然也允許失敗的線程放棄操作。
線程安全整數(shù)(AtomicInteger)
AtomicInteger是在 JDK并發(fā)包中的atomic 中的拯腮,可以把它看作一個(gè)整數(shù),與Integer不同的是蚁飒,它是可變的动壤,并且是線程安全的。對其進(jìn)行修改等任何操作都是用 CAS 指令進(jìn)行的淮逻。下面是AtomicInteger 的常用方法:
public final int get() //取得當(dāng)前值 public final void set(int newValue) //設(shè)置當(dāng)前值public final int getAndSet(int newValue) //設(shè)置新值琼懊,并返回舊值public final boolean compareAndSet(int expect,int u) //如果當(dāng)前值為expect,則設(shè)置為upublic final int getAndIncrement() //當(dāng)前值加1爬早,返回舊值public final int getAndDecrement() //當(dāng)前值減1哼丈,返回舊值public final int getAndAdd(int delta) //當(dāng)前值增加delta。返回舊值public final int incrementAndSet() //當(dāng)前值加1筛严,返回新值public final int decrementAndSet() //當(dāng)前值減1醉旦,返回新值public final int addAndGet(int delta) //當(dāng)前值增加delta,返回新
就內(nèi)部實(shí)現(xiàn)上來說,AtomicInteger中保存了一個(gè)核心字段:
private volatile int value;
使用示例:
可以看出匈棘,在多線程的情況下丧慈,AtomicInteger是保證線程安全的。
無鎖的對象引用(AtomicReference)
AtomicReference 和AtomicInteger非常相似主卫,不同之處就在于AtomicInteger是對整數(shù)的封裝逃默,而AtomicReference是對普通對象的引用,也就是它可以保證你在修改對象引用時(shí)的線程安全性簇搅。
通常情況下線程判斷被修改對象是否可以正確寫入的條件是對象的當(dāng)前值和期望值是否一致是正確的笑旺。但是有一種特殊的情況就是:當(dāng)你獲取對象當(dāng)前數(shù)據(jù)后,在準(zhǔn)備修改被新值前馍资,對象的值被其他線程連續(xù)修改了兩次筒主,最后一次修改為舊值,這個(gè)時(shí)候線程在不知情的情況下鸟蟹,又對這個(gè)數(shù)據(jù)重新賦值乌妙。下圖說明為例:
帶有時(shí)間戳的對象引用(AtomicStampedReference)
AtomicReference無法解決上面的問題是因?yàn)椋瑢ο笤谛薷某沙芍衼G失了狀態(tài)信息熊经,因此我們只要能夠記錄對象在修改過程中的狀態(tài)值泽艘,就可以很好的解決對象被反復(fù)修改的導(dǎo)致線程無法正確判斷對象狀態(tài)的問題。
AtomicStampedReference它內(nèi)部不僅維護(hù)了對象值镐依,還維護(hù)了一個(gè)時(shí)間戳匹涮,當(dāng)AtomicStampedReference被修改的時(shí)候,除了更新數(shù)據(jù)本身外槐壳,還必須要更新時(shí)間戳然低。當(dāng)AtomicStampedReference設(shè)置對象值時(shí),對象值及時(shí)間戳都必須滿足期望值务唐,寫入才會(huì)成功雳攘,因此,即使對象之被反復(fù)讀寫枫笛,寫回原值吨灭,只要時(shí)間戳發(fā)生變化,就不能正確寫入刑巧。
作者:蔡不菜丶
鏈接:https://juejin.im/post/6856219201548664839
來源:掘金