多線程複習筆記

1. 多線程是什麼?    html

    線程就是程序中單獨順序的流控制。線程自己不能運行,它只能用於程序中。多線程則指的是在單個程序中能夠同時運行多個不一樣的線程執行不一樣的任務。java

2. 爲何使用多線程?c++

    多線程編程的目的,就是"最大限度地利用CPU資源",當某一線程的處理不須要佔用CPU,而只和I/O等資源打交道時,讓須要佔用CPU資源的其它線程有機會得到CPU資源。編程

3. 線程與進程有什麼不一樣?緩存

    1). 資源是否獨立:多個進程的內部數據和狀態都是徹底獨立的,而多線程是共享一塊內存空間和一組系統資源,有可能互相影響。線程自己的數據一般只有寄存器數據,以及一個程序執行時使用 的堆棧,安全

          因此線程的切換比進程切換的負擔要小。 多線程

    2). 切換成本:多線程程序比多進程程序須要更少的管理費用。進程是重量級的任務,須要分配給它們獨立的地址空間。進程間通訊是昂貴和受限的。進程間的轉換也是很須要花費的。另外一方面,線程是併發

          輕量級的選手。它們共享相同的地址空間而且共同分享同一個進程。線程間通訊是便宜的,線程間的轉換也是低成本的。 app

4. 骨架框架

    Thread 類也實現了 Runnable 接口,實現了 Runnable 接口中的 run 方法

5. 多線程怎麼用?

    1). 線程的實現有兩種方式,第一種方式是繼承 Thread 類,而後重寫 run 方法;第二種是實現 Runnable 接口,而後實現其 run 方法。 

    2). 將咱們但願線程執行的代碼放到 run 方法中,而後經過 start 方法來啓動線程,start方法首先爲線程的執行準備好系統資源,而後再去調用run方法。當某個類繼承了Thread 類以後,該類就叫作一個線程類。

6. 使用注意事項

    1). 多線程的數量應根據CPU核數以及IO操做的頻繁程度而定。並非線程越多,效率越高。

    2). 中止線程的方式:不能使用 Thread 類的 stop 方法來終止線程的執行。 通常要設定一個變量,在 run 方法中是一個循環,循環每次檢查該變量,若是知足條件則繼續執行,不然跳出循環,線程結束。

    3). 多線程共享資源,執行同一任務,須要Runnable接口。

    4). 關於選擇繼承Thread仍是實現Runnable接口?

         a. Thread和Runnable的區別:若是一個類繼承Thread,則不適合資源共享。可是若是實現了Runable接口的話,則很容易的實現資源共享。

         b. 實現Runnable接口比繼承Thread類所具備的優點:

             1). 適合多個相同的程序代碼的線程去處理同一個資源

             2). 能夠避免java中的單繼承的限制

             3). 增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立。

    5). 在java程序中,只要前臺有一個線程在運行,整個java程序進程不會消失,因此此時能夠設置一個後臺線程,這樣即便java進程消失了,此後臺線程依然可以繼續運行。

    6). getId() 用來獲得線程ID

    7). 基本上全部的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問。

7. 優化

    1). 推薦自定義線程名稱

    2). 使用線程池

8. 監控

    visualVM

 

多線程優化:

1. 給你的線程起個有意義的名字。 這樣能夠方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1. Thread-2 and Thread-3 好多了,給線程起一個和它要完成的任務相關

    的名字,全部的主要框架甚至JDK都遵循這個最佳實踐。

2. 避免鎖定和縮小同步的範圍 鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。所以相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。

3. 多用同步類少用wait 和 notify  首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操做,而用wait和notify很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫

    和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。

4. 多用併發集合少用同步集合 這是另一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,因此在併發編程時使用併發集合效果更好。若是下一次你須要用到map,你應該首先想到用

    ConcurrentHashMap。個人文章Java併發集合有更詳細的說明。

 

 

1. 同步鎖是什麼? 

    基本上全部的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問。一般來講,是在訪問臨界資源的代碼前面加上一個鎖,

    當訪問完臨界資源後釋放鎖,讓其餘線程繼續訪問。在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。

2. 爲何使用同步鎖

    解決線程安全問題

3. synchronized和Lock有什麼不一樣?

    1). Lock不是Java語言內置的,synchronized是Java語言的關鍵字,所以是內置特性。Lock是一個類,經過這個類能夠實現同步訪問;

    2). 採用synchronized不須要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完以後,系統會自動讓線程釋放對鎖的佔用;而Lock則必需要用戶去手動釋放鎖,若是沒有

          主動釋放鎖,就有可能致使出現死鎖現象。

4. 骨架

    1). Lock接口

          a. lock():lock()方法是日常使用得最多的一個方法,就是用來獲取鎖。若是鎖已被其餘線程獲取,則進行等待。         

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}

      b. tryLock():tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不

              到鎖時不會一直在那等待。

          c. tryLock(long time, TimeUnit unit):這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。              

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //若是不能獲取鎖,則直接作其餘事情
}

       d. lockInterruptibly():lockInterruptibly()方法比較特殊,當經過這個方法去獲取鎖時,若是線程正在等待獲取鎖,則這個線程可以響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時

              經過lock.lockInterruptibly()想獲取某個鎖時,倘若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法可以中斷線程B的等待過程。因爲lockInterruptibly()

              的聲明中拋出了異常,因此lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。

          e. unLock()方法是用來釋放鎖的。

          f. newCondition()這個方法

    2). ReentrantLock是惟一實現了Lock接口的類,而且ReentrantLock提供了更多的方法。

    3). ReadWriteLock也是一個接口,在它裏面只定義了兩個方法:readLock()和writeLock()。一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操做分開,分紅2個鎖來分配給線程,從而使得

         多個線程能夠同時進行讀操做。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。

    4). ReentrantReadWriteLock裏面提供了不少豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

5. 同步鎖怎麼用?

    1). synchronized方法 

    2). synchronized代碼塊

    3). Lock用法:         

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"獲得了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

  4). ReentrantReadWriteLock具體用法 

6. 使用注意事項

    1). 在Java中,每個對象都擁有一個鎖標記(monitor),也稱爲監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。

    2). 當一個線程正在訪問一個對象的synchronized方法,那麼其餘線程不能訪問該對象的其餘synchronized方法。這個緣由很簡單,由於一個對象只有一把鎖,當一個線程獲取了該對象的鎖以後,其餘線程

         沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized方法。

  3). 當一個線程正在訪問一個對象的synchronized方法,那麼其餘線程能訪問該對象的非synchronized方法。這個緣由很簡單,訪問非synchronized方法不須要得到該對象的鎖,假如一個方法沒用synchronized

         關鍵字修飾,說明它不會使用到臨界資源,那麼其餘線程是能夠訪問這個方法的,

    4). 有一點要注意:對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,所以不會因爲異常致使出現死鎖現象。

    5). 若是採用Lock,必須主動去釋放鎖,而且在發生異常時,不會自動釋放鎖。所以通常來講,使用Lock必須在try{}catch{}塊中進行,而且將釋放鎖的操做放在finally塊中進行,以保證鎖必定被被釋放,防止死鎖的發生。

    6). 使用讀鎖,能夠大大提高了讀操做的效率。不過要注意的是,若是有一個線程已經佔用了讀鎖,則此時其餘線程若是要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。若是有一個線程已經佔用了寫鎖,

         則此時其餘線程若是申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

7. 優化

    1).  synchronized代碼塊

8. 監控

    visualVM

9. Lock和synchronized的選擇

  總結來講,Lock和synchronized有如下幾點不一樣:

  1). Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;

  2). synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而Lock在發生異常時,若是沒有主動經過unLock()去釋放鎖,則極可能形成死鎖現象,所以使用Lock時須要在finally塊中釋放鎖;

  3). Lock可讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不可以響應中斷;

  4). 經過Lock能夠知道有沒有成功獲取鎖,而synchronized卻沒法辦到。

  5). Lock能夠提升多個線程進行讀操做的效率。

  在性能上來講,若是競爭資源不激烈,二者的性能是差很少的,而當競爭資源很是激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。因此說,在具體使用時要根據適當狀況選擇。

10. 鎖的相關概念介紹

      1). 可重入鎖:若是鎖具有可重入性,則稱做爲可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上代表了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單

           的例子,當一個線程執行到某個synchronized方法時,好比說method1,而在method1中會調用另一個synchronized方法method2,此時線程沒必要從新去申請鎖,而是能夠直接執行方法method2。

      2). 可中斷鎖:可中斷鎖:顧名思義,就是能夠相應中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。若是某一線程A正在執行鎖中的代碼,另外一線程B正在等待獲取該鎖,可能因爲等待時間

           過長,線程B不想等待了,想先處理其餘事情,咱們可讓它中斷本身或者在別的線程中中斷它,這種就是可中斷鎖。在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。

      3). 公平鎖:公平鎖即儘可能以請求鎖的順序來獲取鎖。好比同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最早請求的線程)會得到該所,這種就是公平鎖。非公平鎖即沒法保證鎖的獲取是

           按照請求鎖的順序進行的。這樣就可能致使某個或者一些線程永遠獲取不到鎖。在Java中,synchronized就是非公平鎖,它沒法保證等待的線程獲取鎖的順序。而對於ReentrantLock和ReentrantReadWriteLock,

           它默認狀況下是非公平鎖,可是能夠設置爲公平鎖。咱們能夠在建立ReentrantLock對象時,經過如下方式來設置鎖的公平性:

           ReentrantLock lock = new ReentrantLock(true);

           若是參數爲true表示爲公平鎖,爲fasle爲非公平鎖。默認狀況下,若是使用無參構造器,則是非公平鎖。

      4). 讀寫鎖:讀寫鎖將對一個資源(好比文件)的訪問分紅了2個鎖,一個讀鎖和一個寫鎖。正由於有了讀寫鎖,才使得多個線程之間的讀操做不會發生衝突。ReadWriteLock就是讀寫鎖,它是一個接口,

           ReentrantReadWriteLock實現了這個接口。能夠經過readLock()獲取讀鎖,經過writeLock()獲取寫鎖。

 

 

深刻剖析volatile關鍵字

1.volatile關鍵字的兩層語義

  一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:

  1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。

  2)禁止進行指令重排序。

  先看一段代碼,假如線程1先執行,線程2後執行:

1
2
3
4
5
6
7
8
//線程1
boolean  stop =  false ;
while (!stop){
     doSomething();
}
 
//線程2
stop =  true ;

   這段代碼是很典型的一段代碼,不少人在中斷線程時可能都會採用這種標記辦法。可是事實上,這段代碼會徹底運行正確麼?即必定會將線程中斷麼?不必定,也許在大多數時候,這個代碼可以把線程中斷,

      可是也有可能會致使沒法中斷線程(雖然這個可能性很小,可是隻要一旦發生這種狀況就會形成死循環了)。

  下面解釋一下這段代碼爲什麼有可能致使沒法中斷線程。在前面已經解釋過,每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。

  那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。

  可是用volatile修飾以後就變得不同了:

  第一:使用volatile關鍵字會強制將修改的值當即寫入主存

  第二:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

  第三:因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。

  那麼在線程2修改stop值時(固然這裏包括2個操做,修改線程2工做內存中的值,而後將修改後的值寫入內存),會使得線程1的工做內存中緩存變量stop的緩存行無效,而後線程1讀取時,發現本身的緩存行無效,

      它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值。

  那麼線程1讀取到的就是最新的正確的值。

2. volatile保證原子性嗎?

  從上面知道volatile關鍵字保證了操做的可見性,可是volatile能保證對變量的操做是原子性嗎?

  下面看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  class  Test {
     public  volatile  int  inc =  0 ;
     
     public  void  increase() {
         inc++;
     }
     
     public  static  void  main(String[] args) {
         final  Test test =  new  Test();
         for ( int  i= 0 ;i< 10 ;i++){
             new  Thread(){
                 public  void  run() {
                     for ( int  j= 0 ;j< 1000 ;j++)
                         test.increase();
                 };
             }.start();
         }
         
         while (Thread.activeCount()> 1 )   //保證前面的線程都執行完
             Thread.yield();
         System.out.println(test.inc);
     }
}

    你們想一下這段程序的輸出結果是多少?也許有些朋友認爲是10000。可是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。

  可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有10個線程分別進行了1000次操做,那麼最終inc的值應該是1000*10=10000。

  這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性

  在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:

  假如某個時刻變量inc的值爲10,

  線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;

  而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。

  而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。

  那麼兩個線程分別進行了一次自增操做後,inc只增長了1。

  解釋到這裏,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?而後其餘線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,可是要注意,

      線程1對變量進行讀取操做以後,被阻塞了的話,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值。

  根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。

  把上面的代碼改爲如下任何一種均可以達到效果:

  採用synchronized:

      採用Lock:

      採用AtomicInteger:      

      在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是

      原子性操做。atomic是利用CAS來實現原子性操做的(Compare And Swap),CAS其實是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操做。

3.volatile能保證有序性嗎?

  在前面提到volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。

  volatile關鍵字禁止指令重排序有兩層意思:

  1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

  2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

  可能上面說的比較繞,舉個簡單的例子:

1
2
3
4
5
6
7
8
//x、y爲非volatile變量
//flag爲volatile變量
 
x =  2 ;         //語句1
y =  0 ;         //語句2
flag =  true ;   //語句3
x =  4 ;          //語句4
y = - 1 ;        //語句5

   因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會講語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。

  而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。

  那麼咱們回到前面舉的一個例子:

1
2
3
4
5
6
7
8
9
//線程1:
context = loadContext();    //語句1
inited =  true ;              //語句2
 
//線程2:
while (!inited ){
   sleep()
}
doSomethingwithconfig(context);

   前面舉這個例子的時候,提到有可能語句2會在語句1以前執行,那麼久可能致使context還沒被初始化,而線程2中就使用未初始化的context去進行操做,致使程序出錯。

  這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,由於當執行到語句2時,一定能保證context已經初始化完畢。

4.volatile的原理和實現機制

  前面講述了源於volatile關鍵字的一些使用,下面咱們來探討一下volatile到底如何保證可見性和禁止指令重排序的。

  下面這段話摘自《深刻理解Java虛擬機》:

  「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」

  lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

  2)它會強制將對緩存的修改操做當即寫入主存;

  3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

 

使用volatile關鍵字的場景

synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,

由於volatile關鍵字沒法保證操做的原子性。一般來講,使用volatile必須具有如下2個條件:

1)對變量的寫操做不依賴於當前值

2)該變量沒有包含在具備其餘變量的不變式中

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

事實上,個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。

 

Java中使用volatile的幾個場景。

1. 狀態標記量

2. double check

具體參見:http://www.cnblogs.com/dolphin0520/p/3920373.html

 

 

 

CAS (Central Authentication Service)  單點登陸

CAS (Compare And Swap)

相關文章
相關標籤/搜索