1.鎖優化的思路和方法
1.1 減少鎖持有時間
如果需要同步的代碼只是其中小部分,最好用同步塊代替同步方法,並且儘可能減少同步塊裏的代碼量。
1.2減小鎖粒度
將大對象,拆成小對象,大大增加並行度,降低鎖競爭
偏向鎖,輕量級鎖成功率提高
ConcurrentHashMap的實現
– 若干個Segment :Segment[] segments
– Segment中維護HashEntry
– put操作時
• 先定位到Segment,鎖定一個Segment,執行put
在減小鎖粒度後, ConcurrentHashMap允許若干個線程同時進入
1.3鎖分離
根據功能進行鎖分離
合理使用ReadWriteLock
讀多寫少的情況,可以提高性能
讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離
1.4鎖粗化
通常情況下,爲了保證多線程間的有效併發,會要求每個線程持有鎖的時間儘量短,即在使用完 公共資源後,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能儘早的獲得資源執行 任務。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗 系統寶貴的資源,反而不利於性能的優化。
即在一個方法內有多個同步塊,並且都是用同一個鎖,而每個同步塊之間的非同步代碼執行的是耗時短的操作,應該將多個同步塊合併優化成一個同步塊。
1.5鎖消除
在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作。
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < CIRCLE; i++) {
craeteStringBuffer("JVM", "Diagnosis");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}
public static String craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
2. 虛擬機內的鎖優化
對象頭Mark
Mark Word,對象頭的標記,32位
描述對象的hash、鎖信息,垃圾回收標記,年齡
– 指向鎖記錄的指針 – 指向monitor的指針
– GC標記
– 偏向鎖線程ID
偏向鎖
設計:
每次加鎖/解鎖都會涉及到一些CAS操作(比如對等待隊列的CAS操作),CAS操作會延遲本地調用,因此偏向鎖的想法是一旦線程第一次獲得了監視對象,之後讓監視對象“偏向”這個線程,之後的多次調用則可以避免CAS操作,說白了就是置個變量,如果發現爲true則無需再走各種加鎖/解鎖流程。
大部分情況是沒有競爭的,所以可以通過偏向來提高性能。
所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的線程。
將對象頭Mark的標記設置爲偏向,並將線程ID寫入對象頭Mark。
只要沒有競爭,獲得偏向鎖的線程,將來進入同步塊不需要做同步
當其他線程請求相同的鎖時,偏向模式結束,在多爭用的場景下,如果另外一個線程爭用偏向對象,擁有者需要釋放偏向鎖,而釋放的過程會帶來一些性能開銷,但總體說來偏向鎖帶來的好處還是大於CAS代價的
-XX:+UseBiasedLocking
– 默認啓用
在競爭激烈的場合,偏向鎖會增加系統負擔
public static List<Integer> numberList =new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
long begin=System.currentTimeMillis();
int count=0;
int startnum=0;
while(count<10000000){
numberList.add(startnum);
startnum+=2;
count++;
}
long end=System.currentTimeMillis();
System.out.println(end-begin);
}
本例中,使用偏 向鎖,可以獲得 5%以上的性能 提升
輕量級鎖
BasicObjectLock
– 嵌入在線程棧中的對象
普通的鎖處理性能不夠理想,輕量級鎖是一種快速的鎖定方法。
如果對象沒有被鎖定
– 將對象頭的Mark指針保存到鎖對象中
– 將對象頭設置爲指向鎖的指針(在線程棧空間中)
如果輕量級鎖失敗,表示存在競爭,升級爲重量級鎖(常規鎖)
在沒有鎖競爭前提下,減少傳統鎖使用OS互斥量產生的性能損耗
在競爭激烈時,輕量級鎖會多做很多額外操作,導致性能下降
自旋鎖
原理:
當發生爭用時,若Owner線程能在很短的時間內釋放鎖,則那些正在爭用線程可以稍微等一等(自旋),在Owner線程釋放鎖後,爭用線程可能會立即得到鎖,從而避免了系統阻塞。但Owner運行的時間可能會超出了臨界值,爭用線程自旋一段時間後還是無法獲得鎖,這時爭用線程則會停止自旋進入阻塞狀態(後退)。
何時使用了自旋鎖:
在線程進入ContentionList時,也即第一步操作前。線程在進入等待隊列時首先進行自旋嘗試獲得鎖,如果不成功再進入等待隊列。這對那些已經在等待隊列中的線程來說,稍微顯得不公平。還有一個不公平的地方是自旋線程可能會搶佔了Ready線程的鎖。自旋鎖由每個監視對象維護,每個監視對象一個。
JDK1.6中-XX:+UseSpinning開啓
JDK1.7中,去掉此參數,改爲內置實現
如果同步塊很長,自旋失敗,會降低系統性能
如果同步塊很短,自旋成功,節省線程掛起切換時間,提升性能
偏向鎖,輕量級鎖,自旋鎖總結
不是Java語言層面的鎖優化方法
內置於JVM中的獲取鎖的優化方法和獲取鎖的步驟
每一個線程在準備獲取共享資源時:
第一步,檢查MarkWord裏面是不是放的自己的ThreadId ,如果是,表示當前線程是處於 “偏向鎖”
第二步,如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的線程根據MarkWord裏面現有的ThreadId,通知之前線程暫停,
之前線程將Markword的內容置爲空。
第三步,兩個線程都把對象的HashCode複製到自己新建的用於存儲鎖的記錄空間,接着開始通過CAS操作,
把共享對象的MarKword的內容修改爲自己新建的記錄空間的地址的方式競爭MarkWord,
第四步,第三步中成功執行CAS的獲得資源,失敗的則進入自旋
第五步,自旋的線程在自旋過程中,成功獲得資源(即之前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於 輕量級鎖的狀態,如果自旋失敗
第六步,進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待之前線程執行完成並喚醒自己
– 偏向鎖可用會先嚐試偏向鎖
– 輕量級鎖可用會先嚐試輕量級鎖
– 以上都失敗,嘗試自旋鎖
– 再失敗,嘗試普通鎖,使用OS互斥量在操作系統層掛起
4. 一個錯誤使用鎖的案例
public class IntegerLock {
static Integer i=0;
public static class AddThread extends Thread{
public void run(){
for(int k=0;k<100000;k++){
synchronized(i){
i++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread t1=new AddThread();
AddThread t2=new AddThread();
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
代碼中使用變量i作爲鎖對象,會導致該同步是沒意義的。
因爲變量i的類型是Integer,而i++操作實際上是先+1,然後再賦值給I,通過new Integer()的方式,所以每次i++操作之後,i的對象都被改變了,同步塊中的鎖對象永遠都不會相同。
5. ThreadLocal
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i=i;}
public void run() {
try {
Date t=sdf.parse("2015-03-29 19:29:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
該代碼是爲每一個線程分配一個實例,但是SimpleDateFormat是線程不安全的,因爲SimpleDateFormat裏有個Calendar用來保存和處理日期信息,在多線程中可能會出現A線程調用parse()或format()時返回結果是線程B處理 的日期。
解決方法:
爲每一個線程分配一個實例
static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i=i;}
public void run() {
try {
if(tl.get()==null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
需要注意的是,這裏的ThreadLocal.set(SimpleDateFormat)的時候不能傳入SimpleDateFormat的靜態對象,否則每個線程都是保存了一個對象而已。造成了ThreadLocal沒意義。