Java核心技術點之多線程

[ 本文主要從總體上介紹Java中的多線程技術,對於一些重要的基礎概念會進行相對詳細的介紹,如有敘述不清晰以及不合理的地方,但願你們指出,謝謝你們:) ]html

1、爲何使用多線程

1. 併發與並行

    咱們知道,在單核機器上,「多進程」並非真正的多個進程在同時執行,而是經過CPU時間分片,操做系統快速在進程間切換而模擬出來的多進程。咱們一般把這種狀況成爲併發,也就是多個進程的運行行爲是「一併發生」的,但不是同時執行的,由於CPU核數的限制(PC和通用寄存器只有一套,嚴格來講在同一時刻只能存在一個進程的上下文)。
    如今,咱們使用的計算機基本上都搭載了多核CPU,這時,咱們能真正的實現多個進程並行執行,這種狀況叫作並行,由於多個進程是真正「一併執行」的(具體多少個進程能夠並行執行取決於CPU核數)。綜合以上,咱們知道,併發是一個比並行更加寬泛的概念。也就是說,在單核狀況下,併發只是併發;而在多核的狀況下,併發就變爲了並行。下文中咱們將統一用併發來指代這一律念。

2. 阻塞與非阻塞

    UNIX系統內核提供了一個名爲read的函數,用來讀取文件的內容:
typedef ssize_t int;
typedef size_t unsigned;

ssize_t read(int fd, void *buf, size_t n);
    這個函數從描述符爲fd的當前文件位置複製至多n個字節到內存緩衝區buf。若執行成功則返回讀取到的字節數;若失敗則返回-1。read系統調用默認會 阻塞,也就是說系統會一直等待這個函數執行完畢直到它產生一個返回值。然而咱們知道,磁盤一般是一種慢速I/O設備,這意味着咱們用read函數讀取磁盤文件內容時,每每須要比較長的時間(相對於訪問內存或者計算一些數值來講)。那麼阻塞的時候咱們固然不想讓系統傻等着,咱們想在這期間作點兒別的事情,等着磁盤準備好了通知咱們一下,咱們再來讀取文件內容。實際上,操做系統正是這樣作的。當阻塞在read這類系統調用中的時候,操做系統一般都會讓該進程暫時休眠,調度一個別的進程來執行,以避免乾等着浪費時間,等到磁盤準備好了可讓咱們來進行I/O了,它會發送一箇中斷信號通知操做系統,這時候操做系統從新調度原來的進程來繼續執行read函數。這就是經過多進程實現的併發。
 

3. 多進程 vs 多線程

    進程就是一個執行中的程序實例,而線程能夠看做一個進程的最小執行單元。線程與進程間的一個顯著區別在於每一個進程都有一整套變量,而同一個進程間的多個線程共享該進程的數據。多進程實現的併發一般在進程建立以及數據共享等方面的開銷要比多線程更大,線程的實現一般更加輕量,相應的開銷也就更小,所以在通常客戶端開發場景下,咱們更加傾向於使用多線程來實現併發。
    然而,有時候,多線程共享數據的便捷容易可能會成爲一個讓咱們頭疼的問題,咱們在後文中會具體提到常見的問題及相應的解決方案。在上面的read函數的例子中,若是咱們使用多線程,可使用一個主線程去進行I/O的工做,再用一個或幾個工做線程去執行一些輕量計算任務,這樣當主線程阻塞時,線程調度程序會調度咱們的工做線程來執行計算任務,從而更加充分的利用CPU時間片。並且,在多核機器上,咱們的多個線程能夠並行執行在多個核上,進一步提高效率。
 

2、如何使用多線程

1. 線程執行模型

    每一個進程剛被建立時都只含有一個線程,這個線程一般被稱做主線程(main thread)。然後隨着進程的執行,若遇到建立新線程的代碼,就會建立出新線程,然後隨着新線程被啓動,多個線程就會併發地運行。某時刻,主線程阻塞在一個慢速系統調用中(好比前面提到的read函數),這時線程調度程序會讓主線程暫時休眠, 調度另外一個線程來做爲當前運行的線程。每一個線程也有本身的一套變量,但相比於進程來講要少得多,所以線程切換的開銷更小。
 

2. 建立一個新線程

(1)經過實現Runnable接口    

    在Java中,有兩種方法能夠建立一個新線程。第一種方法是定義一個實現Runnable接口的類並實例化,而後將這個對象傳入Thread的構造器來建立一個新線程,如如下代碼所示:
class MyRunnable implements Runnable {
     ...
    public void run() {
         //這裏是新線程須要執行的任務
    }
}
 
Runnable r = new MyRunnable();
Thread t = new Thread(r);

 

(2)經過繼承Thread類    

    第二種建立一個新線程的方法是直接定義一個Thread的子類並實例化,從而建立一個新線程。好比如下代碼:
class MyThread extends Thread {
    public void run() {
        //這裏是線程要執行的任務
    }
}
    建立了一個線程對象後,咱們直接對其調用start方法便可啓動這個線程:
t.start();

 

(3)兩種方式的比較     

    既然有兩種方式能夠建立線程,那麼咱們該使用哪種呢?首先,直接繼承Thread類的方法看起來更加方便,但它存在一個侷限性:因爲Java中不容許多繼承,咱們自定義的類繼承了Thread後便不能再繼承其餘類,這在有些場景下會很不方便;實現Runnable接口的那個方法雖然稍微繁瑣些,可是它的優勢在於自定義的類能夠繼承其餘的類。java


3. 線程的屬性

(1)線程的狀態

    線程在它的生命週期中可能處於如下幾種狀態之一:
  • New(新生):線程對象剛剛被建立出來;
  • Runnable(可運行):在線程對象上調用start方法後,相應線程便會進入Runnable狀態,若被線程調度程序調度,這個線程便會成爲當前運行(Running)的線程;
  • Blocked(被阻塞):若一段代碼被線程A」上鎖「,此時線程B嘗試執行這段代碼,線程B就會進入Blocked狀態;
  • Waiting(等待):當線程等待另外一個線程通知線程調度器一個條件時,它自己就會進入Waiting狀態;
  • Time Waiting(計時等待):計時等待與等待的區別是,線程只等待必定的時間,若超時則再也不等待;
  • Terminated(被終止):線程的run方法執行完畢或者因爲一個未捕獲的異常致使run方法意外終止會進入Terminated狀態。

    後文中若不加特殊說明的話,咱們會用阻塞狀態統一指代Blocked、Waiting、Time Waiting。    編程

 

(2)線程的優先級

    在Java中,每一個線程都有一個優先級,默認狀況下,線程會繼承它的父線程的優先級。能夠用setPriority方法來改變線程的優先級。Java中定義了三個描述線程優先級的常量:MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY。
    每當線程調度器要調度一個新的線程時,它會首先選擇優先級較高的線程。然而線程優先級是高度依賴與操做系統的,在有些系統的Java虛擬機中,甚至會忽略線程的優先級。所以咱們不該該將程序邏輯的正確性依賴於優先級。線程優先級相關的API以下:
void setPriority(int newPriority) //設置線程的優先級,可使用系統提供的三個優先級常量
static void yield() //使當前線程處於讓步狀態,這樣當存在其餘優先級大於等於本線程的線程時,線程調度程序會調用那個線程

 

4. Thread類

    Thread實現了Runnable接口,關於這個類的如下實例域須要咱們瞭解:數組

private volatile char  name[]; //當前線程的名字,可在構造器中指定
private int priority; //當前線程優先級
private Runnable target; //當前要執行的任務
private long tid; //當前線程的ID

 

    Thread類的經常使用方法除了咱們以前提到的用於啓動線程的start外還有:緩存

  • sleep方法,這是一個靜態方法,做用是讓當前線程進入休眠狀態(但線程不會釋放已獲取的鎖),這個休眠狀態其實就是咱們上面提到過的Time Waiting狀態,從休眠狀態「甦醒」後,線程會進入到Runnable狀態。sleep方法有兩個重載版本,聲明分別以下:
public static native void sleep(long millis) throws InterruptedException; //讓當前線程休眠millis指定的毫秒數
public static native void sleep(long millis, int nanos) throws InterruptedException; //在毫秒數的基礎上還指定了納秒數,控制粒度更加精細

 

  •  join方法,這是一個實例方法,在當前線程中對一個線程對象調用join方法會致使當前線程中止運行,等那個線程運行完畢後再接着運行當前線程。也就是說,把當前線程還沒執行的部分「接到」另外一個線程後面去,另外一個線程運行完畢後,當前線程再接着運行。join方法有如下重載版本:
public final synchronized void join() throws InterruptedException 
public final synchronized void join(long millis) throws InterruptedException; 
public final synchronized void join(long millis, int nanos) throws InterruptedException;

        無參數的join表示當前線程一直等到另外一個線程運行完畢,這種狀況下當前線程會處於Wating狀態;帶參數的表示當前線程只等待指定的時間,這種狀況下當前線程會處於Time Waiting狀態。當前線程經過調用join方法進入Time Waiting或Waiting狀態後,會釋放已經獲取的鎖。實際上,join方法內部調用了Object類的實例方法wait,關於這個方法咱們下面會具體介紹。安全

 

  • yield方法,這是一個靜態方法,做用是讓當前線程「讓步」,目的是爲了讓優先級不低於當前線程的線程有機會運行,這個方法不會釋放鎖。

 

  • interrupt方法,這是一個實例方法。每一個線程都有一箇中斷狀態標識,這個方法的做用就是將相應線程的中斷狀態標記爲true,這樣相應的線程調用isInterrupted方法就會返回true。經過使用這個方法,可以終止那些經過調用可中斷方法進入阻塞狀態的線程。常見的可中斷方法有sleep、wait、join,這些方法的內部實現會時不時的檢查當前線程的中斷狀態,若爲true會馬上拋出一個InterruptedException異常,從而終止當前線程。

     如下這幅圖很好的詮釋了隨着各類方法的調用,線程在不一樣的狀態之間的切換(圖片來源:http://www.cnblogs.com/dolphin0520/p/3920357.html):數據結構

 

5. wait方法與notify/notifyAll方法

(1)wait方法

    wait方法是Object類中定義的實例方法。在指定對象上調用wait方法可以讓當前線程進入阻塞狀態(前提時當前線程持有該對象的內部鎖(monitor)),此時當前線程會釋放已經獲取的那個對象的內部鎖,這樣一來其餘線程就能夠獲取這個對象的內部鎖了。當其餘線程獲取了這個對象的內部鎖,進行了一些操做後能夠調用notify方法來喚醒正在等待該對象的線程。多線程

(2)notify/notifyAll方法

    notify/notifyAll方法也是Object類中定義的實例方法。它倆的做用是喚醒正在等待相應對象的線程,區別在於前者喚醒一個等待該對象的線程,然後者喚醒全部等待該對象的線程。這麼說比較抽象,下面咱們來舉一個具體的例子來講明如下wait和notify/notifyAll的用法。請看如下代碼(轉自Java併發編程:線程間協做的兩種方式):併發

 1 public class Test {
 2     private int queueSize = 10;
 3     private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
 4       
 5     public static void main(String[] args)  {
 6         Test test = new Test();
 7         Producer producer = test.new Producer();
 8         Consumer consumer = test.new Consumer();
 9           
10         producer.start();
11         consumer.start();
12     }
13       
14     class Consumer extends Thread{
15           
16         @Override
17         public void run() {
18             consume();
19         }
20           
21         private void consume() {
22             while(true){
23                 synchronized (queue) {
24                     while(queue.size() == 0){
25                         try {
26                             System.out.println("隊列空,等待數據");
27                             queue.wait();
28                         } catch (InterruptedException e) {
29                             e.printStackTrace();
30                             queue.notify();
31                         }
32                     }
33                     queue.poll();          //每次移走隊首元素
34                     queue.notify();
35                     System.out.println("從隊列取走一個元素,隊列剩餘"+queue.size()+"個元素");
36                 }
37             }
38         }
39     }
40       
41     class Producer extends Thread{
42           
43         @Override
44         public void run() {
45             produce();
46         }
47           
48         private void produce() {
49             while(true){
50                 synchronized (queue) {
51                     while(queue.size() == queueSize){
52                         try {
53                             System.out.println("隊列滿,等待有空餘空間");
54                             queue.wait();
55                         } catch (InterruptedException e) {
56                             e.printStackTrace();
57                             queue.notify();
58                         }
59                     }
60                     queue.offer(1);        //每次插入一個元素
61                     queue.notify();
62                     System.out.println("向隊列取中插入一個元素,隊列剩餘空間:"+(queueSize-queue.size()));
63                 }
64             }
65         }
66     }
67 }

    以上代碼描述的是經典的「生產者-消費者」問題。Consumer類表明消費者,Producer類表明生產者。在生產者進行生產以前(對應第48行的produce方法),會獲取queue的內部鎖(monitor)。而後判斷隊列是否已滿,若滿了則沒法再生產,因此在第54行調用queue.wait方法,從而等待在queue對象上。(釋放了queue的內部鎖)此時生產者可以可以獲取queue的monitor從而進入第21行的consume方法,這樣一來它就會經過第33行的queue.poll方法進行消費,因而隊列再也不滿了,接着它在第34行調用queue.notify方法來通知正在等待的生產者,生產者就會從剛纔阻塞的wait方法(第54行)中返回。異步

    同理,當隊列空時,消費者也會等待(第27行)生產者來喚醒(第61行)。

    await方法和signal/signalAll方法是wait方法和notify/notifyAll方法的升級版,在後文中會具體介紹它們與wait、notify/notifyAll之間的關係。

    

6. 如何保證線程安全

    所謂線程安全,指的是當多個線程併發訪問數據對象時,不會形成對數據對象的「破壞」。保證線程安全的一個基本思路就是讓訪問同一個數據對象的多個線程進行「排隊」,一個接一個的來,這樣就不會對數據形成破壞,但帶來的代價是下降了併發性。

(1)race condition(竟爭條件)

    當兩個或兩個以上的線程同時修改同一數據對象時,可能會產生不正確的結果,咱們稱這個時候存在一個 競爭條件(race condition)。在多線程程序中,咱們必需要充分考慮到多個線程同時訪問一個數據時可能出現的各類狀況,確保對數據進行同步存取,以防止錯誤結果的產生。請考慮如下代碼:
public class Counter {
    private long count = 0;
    public void add(long value) {
        this.count = this.count + value;  
    }
}

 

    咱們注意一下改變count值的那一行,一般這個操做不是一步完成的,它大概分爲如下三步:
  • 第一步,把count的值加載到寄存器中;
  • 第二步,把相應寄存器的值加上value的值;
  • 第三步,把寄存器的值寫回count變量。
    咱們能夠編譯以上代碼而後用javap查看下編譯器爲咱們生成的字節碼:


    咱們能夠看到,大體過程和咱們以上描述的基本同樣。那麼咱們考慮下面這樣一個場景:假設count的初值爲0,首先線程A加載了count到寄存器中,而且加上了1,而就當它要寫回以前,線程B進入了add方法,它加載了count到寄存器中(因爲此時線程A尚未把count寫回,所以count仍是0),並加上了2,而後線程B寫回了count。在線程B完成了寫回後,線程調度程序調度了線程A,線程A也寫回了count。注意,此時count的值爲1而不是咱們但願的三。咱們不但願一個線程在執行add方法時被其餘線程打斷,由於這會形成數據的破壞。咱們但願的狀況是這樣的:線程A完整執行完畢add方法後,待count變量的值更新爲1時,線程B開始執行add方法,在線程B完整執行完畢以前, 沒有別的線程可以打斷它,如有別的線程想調用add,也得等線程B執行完畢寫回count值後。
    像add這種方法代碼所在的內存區,咱們稱之爲臨界區(critical area)。對於臨界區,在同一時刻咱們只但願有一個線程可以訪問它,咱們但願在一個線程進入臨界區後把通往這個區的門「上鎖」,離開後把門"解鎖「,這樣當一個線程執行臨界區的代碼時其餘想要進來的線程只能在門外等着,這樣能夠保證了多個線程共享的數據不會被破壞。下面咱們來介紹下爲臨界區「上鎖」的方法。

(2)鎖對象

    Java類庫中爲咱們提供了可以給臨界區「上鎖」的ReentrantLock類,它實現了Lock接口,在進一步介紹ReentrantLock類以前,咱們先來看一下Lock接口的定義:
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

   咱們來分別介紹下Lock接口中發方法:

  • lock方法用來獲取鎖,在鎖被佔用時它會一直阻塞,而且這個方法不能被中斷;
  • lockInterruptibly方法在獲取不到鎖時也會阻塞,它與lock方法的區別在於阻塞在該方法時能夠被中斷;
  • tryLock方法也是用來獲取鎖的,它的無參版本在獲取不到鎖時會馬上返回false,它的計時等待版本會在等待指定時間還獲取不到鎖時返回false,計時等待的tryLock在阻塞期間也可以被中斷。使用tryLock方法的典型代碼以下:
if (myLock.tryLock()) {
    try {
        …
    } finally {
        myLock.unlock();
    }
} else {
    //作其餘的工做
  • unlock方法用來釋放鎖;
  • newCondition方法用來獲取當前鎖對象相關的條件對象,這個在下文咱們會具體介紹。

 

     ReentrantLock類是惟一一個Lock接口的實現類,它的意思是可重入鎖,關於「可重入」的概念咱們下面會進行介紹。有了上面的介紹,理解它的使用方法就很簡單了,好比下面的代碼即完成了給add方法「上鎖」:
Lock myLock = new ReentrantLock();
public void add(long value) {
    myLock.lock();
    try {
        this.count = this.count + value;
    } finally {
        myLock.unlock();
    }
}

    從以上代碼能夠看到,使用ReentrantLock對象來上鎖時只須要先獲取一個它的實例。而後經過lock方法進行上鎖,經過unlock方法進行解鎖。注意,咱們使用了一個try-finally塊,以確保即便發生異常也老是會解鎖,否則其餘線程會一直沒法執行add方法。當一個線程執行完「myLock.lock()」時,它就得到了一個鎖對象,這就至關於它給臨界區上了鎖,其餘線程都沒法進來,只有這個線程執行完「myLock.unlock()"時,釋放了鎖對象,其餘線程才能再經過「myLock.lock()"得到鎖對象,從而進入臨界區。也就是說,當一個線程獲取了鎖對象後,其餘嘗試獲取鎖對象的線程都會被阻塞,進入Blocked狀態,直至獲取鎖對象的線程釋放了鎖對象。
    有了鎖對象,儘管線程A在執行add方法的過程當中被線程調度程序剝奪了運行權,其餘的線程也進入不了臨界區,由於線程A還在持有鎖對象。這樣一來,咱們就很好的保護了臨界區。
    ReentrantLock鎖是 可重入的,這意味着線程能夠重複得到已經持有的鎖,每一個鎖對象內部都持有一個計數,每當線程獲取依次鎖對象,這個計數就加1,釋放一次就減1。只有當計數值變爲0時,才意味着這個線程釋放了鎖對象,這時其餘線程才能夠來獲取。

(3)條件對象

    有些時候,線程進入臨界區後不能當即執行,它須要等某一條件知足後纔開始執行。好比,咱們但願count值大於5的時候才增長它的值,咱們最早想到的是加個條件判斷:
public void add(int value) {    
    if (this.count > 5) {
        this.count = this.count + value;
    }
}
    然而上面的代碼存在一個問題。假設線程A執行完了條件判斷並的值count值大於5,而在此時該線程被線程調度程序中斷執行,轉而調度線程B,線程B對統一counter對象的count值進行了修改,使得它再也不大於5,這時線程調度程序又來調度線程A,線程A剛纔斷定了條件爲真,因此會執行add方法,儘管此時count值已再也不大於5。顯然,這與咱們所但願的狀況的不符的。對於這種問題,咱們想到了能夠在條件判斷先後加鎖與解鎖:
public void add(int value) {
    myLock.lock();
    try {
        while (counter.getCount() <= 5) {
            //等待直到大於5
        }
        this.count = this.count + value;
    } finally {
        myLock.unlock();
    }
}
   在以上代碼中,若線程A發現count值小於等於5,它會一直等到別的線程增長它的值直到它大於5。然而線程A此時持有鎖對象,其餘線程沒法進入臨界區(add方法內部)來改變count的值,因此當線程A進入臨界區時若count小於等於5,線程A會一直在循環中等待,其餘的線程也沒法進入臨界區。這種狀況下, 咱們可使用條件對象來管理那些已經得到了一個鎖卻不能開始幹活的線程。一個鎖對象能夠有一個或多個相關的條件對象,在鎖對象上調用newCondition方法就能夠得到一個條件對象。好比咱們能夠爲「count值大於5」得到一個條件對象:
Condition enoughCount = myLock.newCondition();
    而後,線程A發現count值不夠時,調用「enoughCount.await()」便可,這時它便會進入Waiting狀態,放棄它持有的鎖對象,以便其餘線程可以進入臨界區。當線程B進入臨界區修改了count值後,發現了count值大於5,線程B可經過"enoughCount.signalAll()"來「喚醒全部等待這一條件知足的線程(這裏只有線程A)。此時線程A會從Waiting狀態進入Runnable狀態。當線程A再次被調度時,它便會從await方法返回,從新得到鎖並接着剛纔繼續執行。注意,此時線程A會再次測試條件是否知足,若知足則執行相應操做。也就是說signalAll方法僅僅是通知線程A一聲count的值可能大於5了,應該再測試一下。還有一個signal方法,會隨機喚醒一個正在等待某條件的線程,這個方法的風險在於若隨機喚醒的線程測試條件後發現仍然不知足,它仍是會再次進入Waiting狀態,若之後再也不有線程喚醒它,它便不能再運行了。

(4)synchronized關鍵字

    Java中的每一個對象都有一個內部鎖,這個內部鎖也被稱爲 監視器(monitor);每一個類內部也有一個鎖,用於控制多個線程對其靜態成員的併發訪問。若一個實例方法用synchronized關鍵字修飾,那麼這個對象的內部鎖會「保護」此方法,咱們稱此方法爲同步方法。這意味着只有獲取了該對象內部鎖的線程纔可以執行此方法。也就是說,如下的代碼:
public synchronized void add(int value) {
    ...
}
等價於:
public void add(int value) {
    this.innerLock.lock();
    try {
        ...
    } finally {
        this.innerLock.unlock();
    }
}
    這意味着,咱們經過給add方法加上synchronized關鍵字便可保護它,加鎖解鎖的工做不須要咱們再手動完成。對象的內部鎖在同一時刻只能由一個線程持有,其餘嘗試獲取的線程都會被阻塞直至該線程釋放鎖, 這種狀況下被阻塞的線程沒法被中斷
    
    內部鎖對象只有一個相關條件。 wait方法添加一個線程到這個條件的等待集中notifyAll / notify方法會喚醒等待集中的線程。也就是說wait() / notify()等價於enoughCount.await() / enoughCount.signAll()。以上add方法咱們能夠這麼實現:
public synchronized void add(int value) {
    while (this.count <= 5) {
        wait(); 
    }
    this.count += value;
    notifyAll();
}

 

    這份代碼顯然比咱們上面的實現要簡潔得多,實際開發中也更加經常使用。
    
    咱們也能夠用synchronized關鍵字修飾靜態方法,這樣的話,進入該方法的線程或獲取相關類的Class對象的內部鎖。例如,若Counter中含有一個synchronized關鍵字修飾的靜態方法,那麼進入該方法的線程會得到Bank.class的內部鎖。這意味着其餘任何線程不能執行Counter類的任何同步靜態方法。
   
     對象內部鎖存在一些侷限性:
  • 不能中斷一個正在試圖獲取鎖的線程;
  • 試圖獲取鎖時不能設定超時;
  • 每一個鎖僅有一個相關條件;
     那麼咱們究竟應該使用Lock/Condition仍是synchronized關鍵字呢?答案是能不用盡可能都不用,咱們應儘量使用java.util.concurrent包中提供給咱們的相應機制(後面會介紹)。
     當咱們要在synchronized關鍵字與Lock間作出選擇時咱們須要考慮如下幾點:
  • 若咱們須要多個線程進行讀操做,應該使用實現了Lock接口的ReentrantReadWriteLock類,這個類容許多個線程同時讀一個數據對象(這個類的使用後面會介紹);
  • 當咱們須要Lock/Condition的特性時,應該考慮使用它(好比多個條件還有計時等待版本的await函數);
  • 通常場景咱們能夠考慮使用synchronized關鍵字,由於它的簡潔性必定程度上可以減小出錯的可能。關於synchronized關鍵字須要注意的一點是:synchronized方法或者synchronized代碼塊出現異常時,Java虛擬機會自動釋放當前線程已獲取的鎖。
 

(5)同步阻塞

    上面咱們提到了一個線程調用synchronized方法能夠得到對象的內部鎖(前提是還未被其餘線程獲取),得到對象內部鎖的另外一種方法就是經過同步阻塞:
synchronized (obj) {
    //臨界區
}

 

    一個線程執行上面的代碼塊即可以獲取obj對象的內部鎖,直至它離開這個代碼塊纔會釋放鎖。
    咱們常常會看到一種特殊的鎖,以下所示:
public class Counter {
    private Object lock = new Object();

    synchronized (lock) {
        //臨界區
    }
    ...
}

    那麼這種使用這種鎖有什麼好處呢?咱們知道Counter對象只有一個內部鎖,這個內部鎖在同一時刻只能被一個對象持有,那麼設想Counter對象中定義了兩個synchronized方法。在某一時刻,線程A進入了其中一個synchronized方法並獲取了內部鎖,此時線程B嘗試進去另外一個synchronized方法時因爲對象內部鎖尚未被線程A釋放,所以線程B只能被阻塞。然而咱們的兩個synchronized方法是兩個不一樣的臨界區,它們不會相互影響,因此它們能夠在同一時刻被不一樣的線程所執行。這時咱們就可使用如上面所示的顯式的鎖對象,它容許不一樣的方法同步在不一樣的鎖上。

(6)volatile域

    有時候,僅僅爲了同步一兩個實例域就使用synchronized關鍵字或是Lock/Condition,會形成不少沒必要要的開銷。這時候咱們可使用volatile關鍵字,使用volatile關鍵字修飾一個實例域會告訴編譯器和虛擬機這個域可能會被多線程併發訪問,這樣編譯器和虛擬機就能確保它的值老是咱們所指望的。
    volatile關鍵字的實現原理大體是這樣的:咱們在訪問內存中的變量時,一般都會把它緩存在寄存器中,之後再須要讀它的值時,只需從相應寄存器中讀取,若要對該變量進行寫操做,則直接寫相應寄存器,最後寫回該變量所在的內存單元。若線程A把count變量的值緩存在寄存器中,並將count加2(將相應寄存器的值加2),這時線程B被調度,它讀取count變量加2後並寫回。而後線程A又被調度,它會接着剛纔的操做,也就是會把count值寫回,此時線程A是直接把寄存器中的值寫回count所在單元,而這個值是過時的。若count被volatile關鍵字修飾,這個問題即可被圓滿解決。volatile變量有一個性質,就是任什麼時候候讀取它的值時,都會直接去相應內存單元讀取,而不是讀取緩存在寄存器中的值。這樣一來,在上面那個場景中,線程A把count寫回時,會從內存中讀取count最新的值,從而確保了count的值老是咱們所指望的。
    關於volatile關鍵字更加詳細的論述請參考這裏: Java併發編程:volatile關鍵字解析 ,感謝海子同咱們分享了這篇精彩博文:)

(7)死鎖

    假設如今進程中只有線程A和線程B這兩個線程,考慮下面這樣一種情形:

    線程A獲取了counterA對象的內部鎖,線程B獲取了counterB對象的內部鎖。而線程A只有在獲取counterB的內部鎖後才能繼續執行,線程B只有在獲取線程A的內部鎖後才能繼續執行。這樣一來,兩個線程在互相等待對方釋放鎖從而誰也無法繼續執行,這種現象就叫作死鎖(deadlock)。

    除了以上狀況,還有一種相似的死鎖狀況是兩個線程獲取鎖後都不知足條件從而進入條件的等待集中,相互等待對方喚醒本身。

    Java沒有爲解決死鎖提供內在機制,所以咱們只有在開發時格外當心,以免死鎖的發生。關於分析定位程序中的死鎖,你們能夠參考這篇文章:Java Deadlock Example and How to analyze deadlock situation

(8)讀/寫鎖

    若不少線程從一個內存區域讀取數據,但其中只有極少的一部分線程會對其中的數據進行修改,此時咱們但願全部Reader線程共享數據,而全部Writer線程對數據的訪問要互斥。咱們可使用讀/寫鎖來達到這一目的。
    Java中的讀/寫鎖對應着ReentrantReadWriteLock類,它實現了ReadWriteLock接口,這個接口的定義以下:
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

     咱們能夠看到這個接口就定義了兩個方法,其中readLock方法用來獲取一個「讀鎖」,writeLock方法用來獲取一個「寫鎖」。

     ReentrantReadWriteLock類的使用步驟一般以下所示:
//構造一個ReentrantReadWriteLock對象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

//分別從中「提取」讀鎖和寫鎖
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

//對全部的Reader線程加讀鎖
readLock.lock();
try {
    //讀操做可併發,但寫操做會互斥
} finally {
    readLock.unlock();
}

//對全部的Writer線程加寫鎖
writeLock.lock();
try {
    //排斥全部其餘線程的讀和寫操做
} finally {
    writeLock.unlock();
}

     在使用ReentrantReadWriteLock類時,咱們須要注意如下兩點:

  • 若當前已經有線程佔用了讀鎖,其餘要申請寫鎖的線程須要佔用讀鎖的線程釋放了讀鎖才能申請成功;
  • 若當前已經有線程佔用了寫鎖,其餘要申請讀鎖或寫鎖的線程都須要等待佔用寫鎖的線程釋放了寫鎖才能申請成功。

 

7. 阻塞隊列

    以上咱們所介紹的都屬於Java併發機制的底層基礎設施。在實際編程咱們應該儘可能避免使用以上介紹的較爲底層的機制,而使用Java類庫中提供給咱們封裝好的較高層次的抽象。對於許多同步問題,咱們能夠經過使用一個或多個隊列來解決:生產者線程向隊列中插入元素,消費者線程則取出他們。考慮一下咱們最開始提到的Counter類,咱們能夠經過隊列來這樣解決它的同步問題:增長計數值的線程不能直接訪問Counter對象,而是把add指令對象插入到隊列中,而後由另外一個可訪問Counter對象的線程從隊列中取出add指令對象並執行add操做(只有這個線程能訪問Counter對象,所以無需採起額外措施來同步)。
    當試圖向滿隊列中添加元素或者向空隊列中移除元素時,阻塞隊列(blocking queue)會致使線程阻塞。經過阻塞隊列,咱們能夠按如下模式來工做:工做者線程能夠週期性的將中間結果放入阻塞隊列中,其餘線程可取出中間結果並進行進一步操做。若前者工做的比較慢(還沒來得及向隊列中插入元素),後者會等待它(試圖從空隊列中取元素從而阻塞);若前者運行的快(試圖向滿隊列中插元素),它會等待其餘線程。阻塞隊列提供瞭如下方法:
  • add方法:添加一個元素。若隊列已滿,會拋出IllegalStateException異常。
  • element方法:返回隊列的頭元素。若隊列爲空,會拋出NoSuchElementException異常。
  • offer方法:添加一個元素,若成功則返回true。若隊列已滿,則返回false。
  • peek方法:返回隊列的頭元素。若隊列爲空,則返回null。
  • poll方法:刪除並返回隊列的頭元素。若隊列爲空,則返回null。
  • put方法:添加一個元素。若隊列已滿,則阻塞。
  • remove方法:移除並返回頭元素。若隊列爲空,會拋出NoSuchElementException。
  • take方法:移除並返回頭元素。若隊列爲空,則阻塞。
    java.util.concurrent包提供瞭如下幾種阻塞隊列:
  • LinkedBlockingQueue是一個基於鏈表實現的阻塞隊列。默認容量沒有上限,但也有能夠指定最大容量的構造方法。它有的「雙端隊列版本」爲LinkedBlockingDeque。
  • ArrayBlockingQueue是一個基於數組實現的阻塞隊列,它在構造時須要指定容量。它還有一個構造方法能夠指定一個公平性參數,若這個參數爲true,那麼等待了最長時間的線程會獲得優先處理(指定公平性參數會下降性能)。
  • PriorityBlockingQueue是一個基於堆實現的帶優先級的阻塞隊列。元素會按照它們的優先級被移除隊列。
    下面咱們來看一個使用阻塞隊列的示例:
public class BlockingQueueTest {
    private int size = 20;
    private ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(size);
     
    public static void main(String[] args)  {
        BlockingQueueTest test = new BlockingQueueTest();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
         
        producer.start();
        consumer.start();
    }
     
    class Consumer extends Thread{
        @Override
        public void run() {
             while(true){
                try {
                    //從阻塞隊列中取出一個元素
                    queue.take();
                    System.out.println("隊列剩餘" + queue.size() + "個元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     
    class Producer extends Thread{         
        @Override
        public void run() {
            while (true) {
                try {
                    //向阻塞隊列中插入一個元素
                    queue.put(1);
                    System.out.println("隊列剩餘空間:" + (size - queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
    在以上代碼中,咱們有一個生產者線程不斷地向一個阻塞隊列中插入元素,同時消費者線程從這個隊列中取出元素。若生產者生產的比較快,消費者取的比較慢致使隊列滿,此時生產者再嘗試插入時就會阻塞在put方法中,直到消費者取出一個元素;反過來,若消費者消費的比較快,生產者生產的比較慢致使隊列空,此時消費者嘗試從中取出時就會阻塞在take方法中,直到生產者插入一個元素。
 

8. 執行器

    建立一個新線程涉及和操做系統的交互,所以會產生必定的開銷。在有些應用場景下,咱們會在程序中建立大量生命週期很短的線程,這時咱們應該使用線程池(thread pool)。一般,一個線程池中包含一些準備運行的空閒線程,每次將Runnable對象交給線程池,就會有一個線程執行run方法。當run方法執行完畢時,線程不會進入Terminated
狀態,而是在線程池中準備等下一個Runnable到來時提供服務。使用線程池統一管理線程能夠減小併發線程的數目,線程數過多每每會在線程上下文切換上以及同步操做上浪費過多時間。
    執行器類(java.util.concurrent.Executors)提供了許多靜態工廠方法來構建線程池。
 
(1)線程池
    在Java中,線程池一般指一個ThreadPoolExecutor對象,ThreadPoolExecutor類繼承了AbstractExecutorService類,而AbstractExecutorService抽象類實現了ExecutorService接口,ExecutorService接口又擴展了Executor接口。也就是說,Executor接口是Java中實現線程池的最基本接口。咱們在使用線程池時一般不直接調用ThreadPoolExecutor類的構造方法,二回使用Executors類提供給咱們的靜態工廠方法,這些靜態工廠方法內部會調用ThreadPoolExecutor的構造方法,併爲咱們準備好相應的構造參數。
    Executor是類中的如下三個方法會返回一個實現了ExecutorService接口的ThreadPoolExecutor類的對象:
ExecutorService newCachedThreadPool() //返回一個帶緩存的線程池,該池在必要的時候建立線程,在線程空閒60s後終止線程
ExecutorService newFixedThreadPool(int threads) //返回一個線程池,線程數目由threads參數指明
ExecutorService newSingleThreadExecutor() //返回只含一個線程的線程池,它在一個單一的線程中依次執行各個任務
  • 對於newCachedThreadPool方法返回的線程池:對每一個任務,如有空閒線程可用,則當即讓它執行任務;若沒有可用的空閒線程,它就會建立一個新線程並加入線程池中;
  • newFixedThreadPool方法返回的線程池裏的線程數目由建立時指定,並一直保持不變。若提交給它的任務多於線程池中的空閒線程數目,那麼就會把任務放到隊列中,當其餘任務執行完畢後再來執行它們;
  • newSingleThreadExecutor會返回一個大小爲1的線程池,由一個線程執行提交的任務。
    
   如下方法可將一個Runnable對象或Callable對象提交給線程池:
Future<T> submit(Callable<T> task)
Future<T> submit(Runnable task, T result)
Future<?> submit(Runnable task)
    調用submit方法會返回一個Future對象,可經過這個對象查詢該任務的狀態。咱們能夠在這個Future對象上調用isDone、cancle、isCanceled等方法(Future接口會在下面進行介紹)。第一個submit方法提交一個Callable對象到線程池中;第二個方法提交一個Runnable對象,而且Future的get方法在完成的時候返回指定的result對象。
    當咱們使用完線程池時,就調用shutdown方法,該方法會啓動該線程池的關閉例程。被關閉的線程池不能再接受新的任務,當關閉前已存在的任務執行完畢後,線程池死亡。shutdownNow方法能夠取消線程池中還沒有開始的任務並嘗試中斷全部線程池中正在運行的線程。
    在使用線程池時,咱們一般應該按照如下步驟來進行:
  • 調用Executors中相關方法構建一個線程池;
  • 調用submit方法提交一個Runnable對象或Callable對象到線程池中;
  • 若想要取消一個任務,須要保存submit返回的Future對象;
  • 當再也不提交任何任務時,調用shutdown方法。

    關於線程池更加深刻及詳細的分析,你們能夠參考這篇博文:http://www.cnblogs.com/dolphin0520/p/3932921.html

    
(2)預約執行
    ScheduledExecutorService接口含有爲 預約執行(Scheduled Execution)或重複執行的任務專門設計的方法。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法會返回實現了ScheduledExecutorService接口的對象。可使用如下方法來預約執行的任務:
ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit)
ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit)
//以上兩個方法預約在指定時間事後執行任務
SchedukedFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) //在指定的延遲(initialDelay)事後,週期性地執行給定任務
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) //在指定延遲(initialDelay)事後週期性的執行任務,每兩個任務間的間隔爲delay指定的時間

    

 
(3)控制任務組
    對ExecutorService對象調用invokeAny方法能夠把一個Callable對象集合提交到相應的線程池中執行,並返回某個已經完成的任務的結果,該方法的定義以下:
T invokeAny(Collection<Callable<T>> tasks)
T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)
    該方法能夠指定一個超時參數。這個方法的不足在於咱們沒法知道它返回的結果是哪一個任務執行的結果。若是集合中的任意Callable對象的執行結果都能知足咱們的需求的話,使用invokeAny方法是很好的。
    invokeAll方法也會提交Callable對象集合到相應的線程池中,並返回一個Future對象列表,表明全部任務的解決方案。該方法的定義以下:
List<Future<T>> invokeAll(Collection<Callable<T>> tasks)
List<Future<T>> invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)

 

9. Callable與Future

    咱們以前提到了建立線程的兩種方式,它們有一個共同的缺點,那就是異步方法run沒有返回值,也就是說咱們沒法直接獲取它的執行結果,只能經過共享變量或者線程間通訊等方式來獲取。好消息是經過使用Callable和Future,咱們能夠方便的得到線程的執行結果。
    Callable接口與Runnable接口相似,區別在於它定義的異步方法call有返回值。Callable接口的定義以下:
public interface Callable<V> {
    V call() throws Exception;
}

 

    類型參數V即爲異步方法call的返回值類型。
 
    Future能夠對具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成以及獲取結果。能夠經過get方法獲取執行結果,該方法會阻塞直到任務返回結果。Future接口的定義以下:
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

 

    在Future接口中聲明瞭5個方法,每一個方法的做用以下:
  • cancel方法用來取消任務,若是取消任務成功則返回true,若是取消任務失敗則返回false。參數mayInterruptIfRunning表示是否容許取消正在執行卻沒有執行完畢的任務,若是設置true,則表示能夠取消正在執行過程當中的任務。若是任務已經完成,則不管mayInterruptIfRunning爲true仍是false,此方法確定返回false(即若是取消已經完成的任務會返回false);若是任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;若是任務尚未執行,則不管mayInterruptIfRunning爲true仍是false,確定返回true。
  • isCancelled方法表示任務是否被取消成功,若是在任務正常完成前被取消成功,則返回 true。
  • isDone方法表示任務是否已經完成,若任務完成,則返回true;
  • get()方法用來獲取執行結果,這個方法會阻塞,一直等到任務執行完才返回;
  • get(long timeout, TimeUnit unit)用來獲取執行結果,若是在指定時間內,還沒獲取到結果,就直接返回null。

  也就是說Future提供了三種功能

  1. 判斷任務是否完成;
  2. 可以中斷任務;
  3. 可以獲取任務執行結果。

     Future接口的實現類是FutureTask:

public class FutureTask<V> implements RunnableFuture<V>

 

    FutureTask類實現了RunnableFuture接口,這個接口的定義以下:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
    能夠看到RunnableFuture接口擴展了Runnable接口和Future接口。

 

    FutureTask類有以下兩個構造器:

public FutureTask(Callable<V> callable) 
public FutureTask(Runnable runnable, V result) 

     FutureTask一般與線程池配合使用,一般會建立一個包裝了Callable對象的FutureTask實例,並用submit方法將它提交到一個線程池去執行,咱們能夠經過FutureTask的get方法獲取返回結果。

 

10. 同步容器與併發容器

(1)同步容器

    Java中的同步容器指的是線程安全的集合類,同步容器主要包含如下兩類:

  • 經過Collections類中的相應方法把普通容器類包裝成線程安全的版本;
  • Vector、HashTable等系統爲咱們封裝好的線程安全的集合類。

    相比與併發容器(下面會介紹),同步容器存在如下缺點:

  • 對於併發讀訪問的支持不夠好;
  • 因爲內部多采用synchronized關鍵字實現,因此性能上不如併發容器;
  • 對同步容器進行迭代的同時修改它的內容,會報ConcurrentModificationException異常。

    關於同步容器更加詳細的介紹請參考這裏:http://www.cnblogs.com/dolphin0520/p/3933404.html

    

(2)併發容器

    併發容器相比於同步容器,具備更強的併發訪問支持,主要體如今如下方面:

  • 在迭代併發容器時修改其內容並不會拋出ConcurrentModificationException異常;
  • 在併發容器的內部實現中儘可能避免了使用synchronized關鍵字,從而加強了併發性。

    Java在java.util.concurrent包中提供了主要如下併發容器類:

  • ConcurrentHashMap,這個併發容器是爲了取代同步的HashMap;
  • CopyOnWriteArrayList,使用這個類在迭代時進行修改不拋異常;
  • ConcurrentLinkedQuerue是一個非阻塞隊列;
  • ConcurrentSkipListMap用於在併發環境下替代SortedMap;
  • ConcurrentSkipSetMap用於在併發環境下替代SortedSet。

    關於這些類的具體使用,你們能夠參考官方文檔及相關博文。一般來講,併發容器的內部實現作到了併發讀取不用加鎖,併發寫時加鎖的粒度儘量小。

11. 同步器(Synchronizer)

    java.util.concurrent包提供了幾個幫助咱們管理相互合做的線程集的類,這些類的主要功能和適用場景以下:

  • CyclicBarrier:它容許線程集等待直至其中預約數目的線程到達某個狀態(這個狀態叫公共障柵(barrier)),而後能夠選擇執行一個處理障柵的動做。適用場景:當多個線程都完成某操做,這些線程才能繼續執行時,或都完成了某操做後才能執行指定任務時。對CyclicBarrier對象調用await方法便可讓相應線程進入barrier狀態,等到預約數目的線程都進入了barrier狀態後,這些線程就能夠繼續往下執行了
  • CountDownLatch:容許線程集等待直到計數器減爲0。適用場景:當一個或多個線程須要等待直到指定數目的事件發生。舉例來講,假如主線程須要等待N個子線程執行完畢才繼續執行,就可使用CountDownLatch來實現,須要用到CountDownLatch的如下方法:
    1 public void await() throws InterruptedException { };   //調用該方法的線程會進入阻塞狀態,直到count值爲0才繼續執行
    2 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //await方法的計時等待版本
    3 public void countDown() { };  //將CountDownLatch對象count值(初始化時做爲參數傳入構造方法)減1
  • Exchanger:容許兩個線程在要交換的對象準備好時交換對象。適用場景:當兩個線程工做在統一數據結構的兩個實例上時,一個向實例中添加數據,另外一個從實例中移除數據。
  • Semaphore:容許線程集等待直到被容許繼續運行爲止。適用場景:限制同一時刻對某一資源併發訪問的線程數,初始化Semaphore須要指定許可的數目,線程要訪問受限資源時須要獲取一個許可,當全部許可都被獲取,其餘線程就只有等待許可被釋放後才能獲取。
  • SynchronousQueue:容許一個線程把對象交給另外一個線程。適用場景:在沒有顯式同步的狀況下,當兩個線程準備好將一個對象從一個線程傳遞到另外一個線程。

 

      關於CountDownLatch、CyclicBarrier、Semaphore的具體介紹和使用示例你們能夠參考這篇博文:Java併發編程:CountDownLatch、CyclicBarrier和Semaphore

 

3、參考資料

  1.  Java併發編程:Callable、Future和FutureTask
  2. Java併發編程:阻塞隊列
  3. 《Java核心技術(卷一)》
  4. 《深刻理解計算機系統(第二版)》
  5. Java併發編程:Lock
相關文章
相關標籤/搜索