一旦用到鎖,就說明這是阻塞式的。這裏提到的鎖優化,是指在阻塞式的狀況下,如何讓性能不要變得太差。可是再怎麼優化,通常來講性能都會比無鎖的狀況差一些。java
public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); } |
上述代碼,線程在進入方法前都要先獲取到鎖,同時其餘線程只能在外面等待。程序員
這裏優化的一點在於,要減小其餘線程等待的時間,因此,只在有線程安全要求的程序上加鎖。數組
public void syncMethod2(){ othercode1(); synchronized(this){ mutextMethod(); } othercode2(); } |
將大對象(這個對象可能會被不少線程訪問),折成小對象,大大增長並行度,下降鎖競爭、下降了鎖的競爭,偏向鎖,輕量級鎖成功率纔會提升。安全
最典型的減小鎖粒度的案例就是ConcurrentHashMap(ConcurrentHashMap內部使用Segment數組,每一個Segment相似於Hashtable。put操做時,先定位到Segment,鎖定一個Segment,執行put)。在減少鎖粒度後, ConcurrentHashMap容許若干個線程同時進入。多線程
最多見的鎖分離就是讀寫鎖ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了線程的安全,又提升了性能。併發
讀寫分離思想能夠延伸,只要操做互不影響,鎖就能夠分離。app
好比:LinkedBlockingQueue(鏈表、隊列)高併發
從頭部取出,從尾部放數據。這有點相似ForkKoinPool中的工做竊取。源碼分析
一般狀況下,爲了保證多線程間的有效併發,會要求每一個線程持有鎖的時間儘可能短,即在使用完公共資源後,應該當即釋放鎖。只有這樣,等待在這個鎖上的其餘線程才能儘早地獲取資源執行任務。可是凡事都有一個度,若是對同一個鎖不停地進行請求、同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能的優化。性能
舉個例子:
public void demoMethod(){ synchronized(lock){ //do sth. } //作其餘不須要的同步的工做,但能很快執行完畢 synchronized(lock){ //do sth. } } |
這種狀況,根據鎖粗化的思想,應該合併:
public void demoMethod(){ //整合成一次鎖請求 synchronized(lock){ //do sth. //作其餘不須要的同步的工做,但能很快執行完畢 } } |
固然這是有前提的,前提就是中間那麼不須要同步的工做是很快執行完成的。
再舉一個極端的例子:
for(int i=0;i<CIRCLE;i++){ synchronized(lock){ } } |
在循環內不停地獲取鎖。雖然JDK內部會對這個代碼作些優化,可是還不如直接寫成
synchronized(lock){ for(int i=0;i<CIRCLE;i++){ } } |
固然若是有需求說,循壞不能讓其餘線程等待過久,那隻能寫成第一種形式。若是沒有這樣相似的需求,仍是直接寫成第二種實現方式比較好。
在即時編譯時,若是發現不可能被共享的對象,則能夠消除這些對象的鎖操做。
也許你會以爲奇怪,既然有些對象不可能被多線程訪問,那爲何要加鎖呢?寫代碼時直接不加鎖不就行了。
可是有些鎖並非程序員所寫的,好比Vector和StringBuffer這樣的類,它們中的不少方法都是有鎖的。當咱們在一些不會有線程安全的狀況下使用這些類的方法時,達到某些條件時,編譯器會將鎖消除來提升性能。
例如:
public static void main(String args[]) throws InterruptedException { public static String createStringBuffer(String s1, String s2) { |
上述代碼中的StringBuffer.append是一個同步操做,可是StringBuffer倒是一個局部變量,而且方法也沒有把StringBuffer返回,因此不可能會有多線程去訪問它。
那麼此時StringBuffer中的同步操做就是沒有意義的。
開啓鎖消除是在JVM參數上設置的,固然須要在server模式下:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks |
而且要開啓逃逸分析。逃逸分析的做用呢,就是看看變量是否有可能逃出做用域的範圍。
好比上述的StringBuffer,上述代碼中createStringBuffer的返回是一個String,因此這個局部變量StringBuffer在其餘地方都不會被使用。若是將createStringBuffer改爲:
public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } |
那麼這個StringBuffer被返回後,是有可能被任何其餘地方所使用的。那麼JVM的逃逸分析能夠分析出,這個局部變量StringBuffer逃出了它的做用域,鎖就不會被消除。
當JVM參數爲:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks |
輸出:
craeteStringBuffer: 302 ms |
JVM參數爲:
-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks |
輸出:
craeteStringBuffer: 660 ms |
顯然,鎖消除的效果仍是很明顯的。
首先要介紹下對象頭,在JVM中,每一個對象都有一個對象頭。
– 指向鎖記錄的指針
– 指向monitor的指針
– GC標記
– 偏向鎖線程ID
簡單來講,對象頭就是要保存一些系統性的信息。
偏向鎖的例子:
package test; import java.util.List; public class Test { public static void main(String[] args) throws InterruptedException { } |
Vector是一個線程安全的類,內部使用了鎖機制。每次add都會進行鎖請求。上述代碼只要main一個線程在反覆add請求鎖。使用以下的JVM參數來設置偏向鎖:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 |
BiasedLockingStartupDelay表示系統啓動幾秒鐘後啓用偏向鎖。默認爲4秒,緣由在於,系統剛啓動時,通常數據競爭是比較激烈的,此時啓用偏向鎖會下降性能。
因爲這裏爲了測試偏向鎖的性能,因此把延遲偏向鎖的時間設置爲0。
輸出:9209
下面關閉偏向鎖:
-XX:-UseBiasedLocking |
輸出:9627
通常在無競爭時,啓用偏向鎖性能會提升5%左右。
Java的多線程安全是基於Lock機制實現的,而Lock的性能每每不如人意。
緣由是,monitorenter與monitorexit這兩個控制多線程同步的bytecode原語,是JVM依賴操做系統互斥(mutex)來實現的。
互斥是一種會致使線程掛起,並在較短的時間內又須要從新調度回原線程的,叫我消耗資源的操做。
輕量級鎖的總結:
當競爭存在時,由於輕量級鎖嘗試失敗,以後有可能會直接升級成重要級鎖動用操做系統層面的互斥,也有可能再嘗試一下自旋鎖。
偏向鎖,輕量級鎖,自旋鎖總結:
public class IntegerLock { public static class AddThread extends Thread { public static void main(String[] args) throws InterruptedException { |
一個很初級的錯誤在於,Integer是final不變的,每次++後,會產生一個新的Integer再賦值給i,因此兩個線程競爭的鎖是不一樣的。因此並非線程安全的。
這裏來提ThreadLocal可能有點不合適,可是ThreadLocal是能夠把鎖代替的方式。因此仍是有必要提一下。
基本的思想就是,在一個多線程當中須要把有數據衝突的數據加鎖,使用ThreadLocal的話,爲每個線程都提供一個對象。不一樣的線程只訪問本身的對象,而不訪問其餘的對象。這樣鎖就不必存在了。
package test; import java.text.ParseException; public class Test { public static class ParseDate implements Runnable { public ParseDate(int i) { public void run() { public static void main(String[] args) { } |
因爲SimpleDateFormat並不線程安全的,因此上述代碼是錯誤的使用。最簡單的方式就是,本身定義一個類去用synchronized包裝(相似於Collections.synchronizedMap)。這樣作在高併發時會有問題,對synchronized的爭用致使每一次只能進去一個線程,併發量很低。這裏使用ThreadLocal去封裝SimpleDateFormat就解決了這個問題。
package test; import java.text.ParseException; public class Test { public static class ParseDate implements Runnable { public ParseDate(int i) { public void run() { public static void main(String[] args) { } |
每一個線程在運行時,會判斷當前線程是否有SimpleDateFormat對象:
if (tl.get() == null) |
若是沒有的話,就new個SimpleDateFormat與當前線程綁定:
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); |
而後用當前線程的SimpleDateFormat去解析:
tl.get().parse("2016-02-16 17:00:" + i % 60); |
一開始的代碼中,只有一個SimpleDateFormat,使用了ThreadLocal,爲每個線程都new了一個SimpleDateFormat。須要注意的是,這裏不要把公共的一個SimpleDateFormat設置給每個ThreadLocal,這樣是沒用的。須要給每個都new一個SimpleDataFormar。
在hibernate中,對ThreadLocal有典型的應用。
下面來看一下ThreadLocal的源碼實現
首先Thread類中有一個成員變量:
ThreadLocal.ThreadLocalMap threadLocals = null; |
而這個Map就是ThreadLocal的實現關鍵:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } |
根據ThreadLocal能夠set和get相對應的value。這裏的ThreadLocalMap實現和HashMap差很少,可是在hash衝突的處理上有區別。ThreadLocalMap中發生hash衝突時,不是像HashMap這樣用鏈表來解決衝突,而是將索引++,放到下一個索引來解決衝突。