多線程之七:鎖優化

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沒意義。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章