程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 高並發九:鎖的優化和留意事項詳解

Java 高並發九:鎖的優化和留意事項詳解

編輯:關於JAVA

Java 高並發九:鎖的優化和留意事項詳解。本站提示廣大學習愛好者:(Java 高並發九:鎖的優化和留意事項詳解)文章只能為提供參考,不一定能成為您想要的結果。以下是Java 高並發九:鎖的優化和留意事項詳解正文


摘要

本系列基於煉數成金課程,為了更好的進修,做了系列的記載。 本文重要引見: 1. 鎖優化的思緒和辦法 2. 虛擬機內的鎖優化 3. 一個毛病應用鎖的案例 4. ThreadLocal及其源碼剖析

1. 鎖優化的思緒和辦法

在[高並發Java 一] 媒介中有提到並發的級別。

一旦用到鎖,就解釋這是壅塞式的,所以在並發度上普通來講都邑比無鎖的情形低一點。

這裡提到的鎖優化,是指在壅塞式的情形下,若何讓機能不要變得太差。然則再怎樣優化,普通來講機能都邑比無鎖的情形差一點。

這裡要留意的是,在[高並發Java 五] JDK並發包1中提到的ReentrantLock中的tryLock,傾向於一種無鎖的方法,由於在tryLock斷定時,其實不會把本身掛起。

鎖優化的思緒和辦法總結一下,有以下幾種。

  1. 削減鎖持有時光
  2. 減小鎖粒度
  3. 鎖分別
  4. 鎖粗化
  5. 鎖清除
  6. 1.1 削減鎖持有時光

    public synchronized void syncMethod(){ 
     othercode1(); 
     mutextMethod(); 
     othercode2(); 
     }
    

    像上述代碼如許,在進入辦法前就要獲得鎖,其他線程就要在裡面期待。

    這裡優化的一點在於,要削減其他線程期待的時光,所以,只用在有線程平安請求的法式上加鎖

    public void syncMethod(){ 
     othercode1(); 
     synchronized(this)
     {
     mutextMethod(); 
     }
     othercode2(); 
     }
    

    1.2 減小鎖粒度

    將年夜對象(這個對象能夠會被許多線程拜訪),拆成小對象,年夜年夜增長並行度,下降鎖競爭。下降了鎖的競爭,傾向鎖,輕量級鎖勝利率才會進步。

    最最典范的減小鎖粒度的案例就是ConcurrentHashMap。這個在[高並發Java 五] JDK並發包1有提到。

    1.3 鎖分別

    最多見的鎖分別就是讀寫鎖ReadWriteLock,依據功效停止分別成讀鎖和寫鎖,如許讀讀不互斥,讀寫互斥,寫寫互斥,即包管了線程平安,又進步了機能,詳細也請檢查[高並發Java 五] JDK並發包1。

    讀寫分別思惟可以延長,只需操作互不影響,鎖便可以分別。

    好比LinkedBlockingQueue 

    從頭部掏出,從尾部放數據。固然也相似於[高並發Java 六] JDK並發包2中提到的ForkJoinPool中的任務盜取。

    1.4 鎖粗化

    平日情形下,為了包管多線程間的有用並發,會請求每一個線程持有鎖的時光盡可能短,即在應用完公共資本後,應當立刻釋放鎖。只要如許,期待在這個鎖上的其他線程能力盡早的取得資本履行義務。然則,凡事都有一個度,假如對統一個鎖一直的停止要求、同步和釋放,其自己也會消費體系名貴的資本,反而晦氣於機能的優化 。

    舉個例子:

    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++){ 
     
     } 
     }
    

    固然假如有需求說,如許的輪回太久,須要給其他線程不要期待太久,那只能寫成下面那種。假如沒有如許相似的需求,照樣直接寫成上面那種比擬好。

    1.5 鎖清除

    鎖清除是在編譯器級其余工作。

    期近時編譯器時,假如發明弗成能被同享的對象,則可以清除這些對象的鎖操作。

    或許你會認為奇異,既然有些對象弗成能被多線程拜訪,那為何要加鎖呢?寫代碼時直接不加鎖不就行了。

    然則有時,這些鎖其實不是法式員所寫的,有的是JDK完成中就有鎖的,好比Vector和StringBuffer如許的類,它們中的許多辦法都是有鎖的。當我們在一些不會有線程平安的情形下應用這些類的辦法時,到達某些前提時,編譯器會將鎖清除來進步機能。

    好比:

    public static void main(String args[]) throws InterruptedException {
     long start = System.currentTimeMillis();
     for (int i = 0; i < 2000000; i++) {
     createStringBuffer("JVM", "Diagnosis");
     }
     long bufferCost = System.currentTimeMillis() - start;
     System.out.println("craeteStringBuffer: " + bufferCost + " ms");
     }
    
     public static String createStringBuffer(String s1, String s2) {
     StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     return sb.toString();
     }
    
    

    上述代碼中的StringBuffer.append是一個同步操作,然則StringBuffer倒是一個部分變量,而且辦法也並沒有把StringBuffer前往,所以弗成能會有多線程去拜訪它。
    那末此時StringBuffer中的同步操作就是沒成心義的。

    開啟鎖清除是在JVM參數上設置的,固然須要在server形式下:

    -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

    而且要開啟逃逸剖析。 逃逸剖析的感化呢,就是看看變量能否有能夠逃出感化域的規模。
    好比上述的StringBuffer,上述代碼中craeteStringBuffer的前往是一個String,所以這個部分變量StringBuffer在其他處所都不會被應用。假如將craeteStringBuffer改成

    public static StringBuffer craeteStringBuffer(String s1, String s2) {
     StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     return sb;
     }
    

    那末這個 StringBuffer被前往後,是有能夠被任何其他處所所應用的(比方被主函數將前往成果put進map啊等等)。那末JVM的逃逸剖析可以剖析出,這個部分變量 StringBuffer逃出了它的感化域。

    所以基於逃逸剖析,JVM可以斷定,假如這個部分變量StringBuffer並沒有逃出它的感化域,那末可以肯定這個StringBuffer其實不會被多線程所拜訪,那末便可以把這些過剩的鎖給去失落來進步機能。

    當JVM參數為:

    -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

    輸入:

    craeteStringBuffer: 302 ms

    JVM參數為:

    -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks

    輸入:

    craeteStringBuffer: 660 ms

    明顯,鎖清除的後果照樣很顯著的。

    2. 虛擬機內的鎖優化

    起首要引見下對象頭,在JVM中,每一個對象都有一個對象頭。

    Mark Word,對象頭的標志,32位(32位體系)。

    描寫對象的hash、鎖信息,渣滓收受接管標志,年紀

    還會保留指向鎖記載的指針,指向monitor的指針,傾向鎖線程ID等。

    簡略來講,對象頭就是要保留一些體系性的信息。

    2.1 傾向鎖

    所謂的傾向,就是偏幸,即鎖會傾向於以後曾經占領鎖的線程 。

    年夜部門情形是沒有競爭的(某個同步塊年夜多半情形都不會湧現多線程同時競爭鎖),所以可以經由過程傾向來進步機能。即在無競爭時,之前取得鎖的線程再次取得鎖時,會斷定能否傾向鎖指向我,那末該線程將不消再次取得鎖,直接便可以進入同步塊。

    傾向鎖的實行就是將對象頭Mark的標志設置為傾向,並將線程ID寫入對象頭Mark

    當其他線程要求雷同的鎖時,傾向形式停止

    JVM默許啟用傾向鎖 -XX:+UseBiasedLocking

    在競爭劇烈的場所,傾向鎖會增長體系累贅(每次都要加一次能否傾向的斷定)

    傾向鎖的例子:

    package test;
    
    import java.util.List;
    import java.util.Vector;
    
    public class Test {
     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);
     }
    
    }
    
    

    Vector是一個線程平安的類,外部應用了鎖機制。每次add都邑停止鎖要求。上述代碼只要main一個線程再重復add要求鎖。
    應用以下的JVM參數來設置傾向鎖:

    -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    BiasedLockingStartupDelay表現體系啟動幾秒鐘後啟用傾向鎖。默許為4秒,緣由在於,體系剛啟動時,普通數據競爭是比擬劇烈的,此時啟用傾向鎖會下降機能。
    因為這裡為了測試傾向鎖的機能,所以把延遲傾向鎖的時光設置為0。

    此時輸入為9209

    上面封閉傾向鎖:

    -XX:-UseBiasedLocking

    輸入為9627

    普通在無競爭時,啟用傾向鎖機能會進步5%閣下。

    2.2 輕量級鎖

    Java的多線程平安是基於Lock機制完成的,而Lock的機能常常不如人意。

    緣由是,monitorenter與monitorexit這兩個掌握多線程同步的bytecode原語,是JVM依附操作體系互斥(mutex)來完成的。

    互斥是一種會招致線程掛起,並在較短的時光內又須要從新調劑回原線程的,較為消費資本的操作。

    為了優化Java的Lock機制,從Java6開端引入了輕量級鎖的概念。

    輕量級鎖(Lightweight Locking)本意是為了削減多線程進入互斥的概率,其實不是要替換互斥。

    它應用了CPU原語Compare-And-Swap(CAS,匯編指令CMPXCHG),測驗考試在進入互斥前,停止解救。

    假如傾向鎖掉敗,那末體系會停止輕量級鎖的操作。它存在的目標是盡量不消動用操作體系層面的互斥,由於誰人機能會比擬差。由於JVM自己就是一個運用,所以願望在運用層面上就處理線程同步成績。

    總結一下就是輕量級鎖是一種疾速的鎖定辦法,在進入互斥之前,應用CAS操作來測驗考試加鎖,盡可能不要用操作體系層面的互斥,進步了機能。

    那末當傾向鎖掉敗時,輕量級鎖的步調:

    1.將對象頭的Mark指針保留到鎖對象中(這裡的對象指的就是鎖住的對象,好比synchronized (this){},this就是這裡的對象)。

    lock->set_displaced_header(mark);

    2.將對象頭設置為指向鎖的指針(在線程棧空間中)。

    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(),mark)) 
     {  
     TEVENT (slow_enter: release stacklock) ;  
     return ; 
     }

    lock位於線程棧中。所以斷定一個線程能否持有這把鎖,只需斷定這個對象頭指向的空間能否在這個線程棧的地址空間傍邊。
    假如輕量級鎖掉敗,表現存在競爭,進級為分量級鎖(慣例鎖),就是操作體系層面的同步辦法。在沒有鎖競爭的情形,輕量級鎖削減傳統鎖應用OS互斥量發生的機能消耗。在競爭異常劇烈時(輕量級鎖老是掉敗),輕量級鎖會多做許多額定操作,招致機能降低。

    2.3 自旋鎖

    當競爭存在時,由於輕量級鎖測驗考試掉敗,以後有能夠會直接進級成分量級鎖動用操作體系層面的互斥。也有能夠再測驗考試一下自旋鎖。

    假如線程可以很快取得鎖,那末可以不在OS層掛起線程,讓線程做幾個空操作(自旋),而且一直地測驗考試拿到這個鎖(相似tryLock),固然輪回的次數是無限制的,當輪回次數到達今後,依然進級成分量級鎖。所以在每一個線程關於鎖的持有時光很少時,自旋鎖可以或許盡可能防止線程在OS層被掛起。

    JDK1.6中-XX:+UseSpinning開啟

    JDK1.7中,去失落此參數,改成內置完成

    假如同步塊很長,自旋掉敗,會下降體系機能。假如同步塊很短,自旋勝利,節儉線程掛起切換時光,晉升體系機能。

    2.4 傾向鎖,輕量級鎖,自旋鎖總結

    上述的鎖不是Java說話層面的鎖優化辦法,是內置在JVM傍邊的。

    起首傾向鎖是為了不某個線程重復取得/釋放統一把鎖時的機能消費,假如依然是同個線程去取得這個鎖,測驗考試傾向鎖時會直接進入同步塊,不須要再次取得鎖。

    而輕量級鎖和自旋鎖都是為了不直接挪用操作體系層面的互斥操作,由於掛起線程是一個很耗資本的操作。

    為了盡可能防止應用分量級鎖(操作體系層面的互斥),起首會測驗考試輕量級鎖,輕量級鎖會測驗考試應用CAS操作來取得鎖,假如輕量級鎖取得掉敗,解釋存在競爭。然則或許很快就可以取得鎖,就會測驗考試自旋鎖,將線程做幾個空輪回,每次輪回時都赓續測驗考試取得鎖。假如自旋鎖也掉敗,那末只能進級成分量級鎖。

    可見傾向鎖,輕量級鎖,自旋鎖都是悲觀鎖。

    3. 一個毛病應用鎖的案例

    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);
     }
    }
    
    

    一個很低級的毛病在於,在 [高並發Java 七] 並發設計形式提到,Interger是final不變的,每次++後,會發生一個新的 Interger再賦給i,所以兩個線程爭取的鎖是分歧的。所以其實不是線程平安的。

    4. ThreadLocal及其源碼剖析

    這裡來提ThreadLocal能夠有點不適合,然則ThreadLocal是可以把鎖取代的方法。所以照樣有需要提一下。

    根本的思惟就是,在一個多線程傍邊須要把稀有據抵觸的數據加鎖,應用ThreadLocal的話,為每個線程都供給一個對象實例。分歧的線程只拜訪本身的對象,而不拜訪其他的對象。如許鎖就沒有需要存在了。

    package test;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Test {
     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("2016-02-16 17:00:" + 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其實不線程平安的,所以上述代碼是毛病的應用。最簡略的方法就是,本身界說一個類去用synchronized包裝(相似於Collections.synchronizedMap)。如許做在高並發時會有成績,對 synchronized的爭用招致每次只能出來一個線程,並發量很低。

    這裡應用ThreadLocal去封裝SimpleDateFormat就處理了這個成績

    package test;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Test {
     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("2016-02-16 17:00:" + 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對象

    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一個SimpleDateFormat。

    在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如許用鏈表來處理抵觸,而是是將索引++,放到下一個索引處來處理抵觸。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved