Java 基礎(十三)執行緒——上

diamond_lin發表於2017-11-01

今天開始正式進入執行緒的學習。

Java 執行緒:執行緒的概念和原理

作業系統中的執行緒和程式的概念

現在的作業系統都是多工的作業系統,多執行緒是多工的一種方式。

程式是指一個記憶體中執行的應用程式,每個程式都有自己獨立的一塊記憶體空間。一個程式可以啟動多個執行緒。

執行緒是指程式中的一個執行流程,一個程式中可以執行多個執行緒。執行緒總是屬於某個程式,程式中的多個執行緒共享程式的記憶體。

多執行緒“同時”執行是人的感覺,其實是執行緒之間輪換執行的。

Java 中的執行緒

在 Java 中,“執行緒”指兩件不同的事情:

1.Thread 類的一個例項
2.執行緒的執行

使用 Thread 類或者 Runnable 介面編寫程式碼來定義、例項化和啟動新執行緒。

一個 Thread 類例項只是一個物件,和 Java 中的任何物件一樣,具有變數和方法,生死與堆上。

Java 中,每個執行緒都有一個呼叫棧,即使不在程式中建立任何新的執行緒,執行緒也在後臺執行著。

一個 Java 應用總是從 main 方法開始執行,main 方法執行在一個執行緒內,它被稱為主執行緒。

一個建立一個新的執行緒,就產生一個新的呼叫棧。

執行緒總體分兩類:使用者執行緒和守護執行緒。

當所有使用者執行緒執行完畢的時候,JVM 自動關閉。但是守護執行緒缺不獨立於 JVM,守護執行緒一般是由作業系統或者使用者自己建立的。

Java 執行緒:建立與啟動

定義執行緒

  • 擴充套件 Thread 類
    此類中有個 run 方法,應該注意其使用方法:
    public void run
    如果該現場是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。
    Thread 的子類應該重寫該方法。

  • 實現 Runnable 介面
    使用實現介面 Runnable 的物件建立一個執行緒時,啟動該執行緒將導致在獨立執行的執行緒中呼叫物件的run 方法。
    方法 run 的常規協定是,它可能執行任何所需的操作。

例項化執行緒

  • 如果是擴充套件 Thread 類的執行緒, 直接 new 即可。
  • 如果是擴充套件 Runnable 類的執行緒,則直接 new 即可。

啟動執行緒

線上程的 Thread 物件上呼叫 start 方法,而不是 run 方法。

在呼叫 start 方法之前:執行緒處於新狀態中,新狀態指有一個 Thread 物件,但還沒有一個真正的執行緒。

在呼叫 start 方法之後,發生了一系列複雜的事情

1.啟動新的執行執行緒(具有新的呼叫棧)
2.該執行緒從新狀態轉移到可執行狀態
3.當該執行緒活得機會執行時,其目標 run 方法將允許。

注意:對於 Java 來說,run 方法沒有任何特別之處。像 main 方法一樣,它只是新執行緒知道呼叫的方法和名稱。因此,Runnable 上或者 Thread 上呼叫 run 方法是合法的,但是並不會啟動新執行緒。

舉例

1.實現 Runnable 介面的多執行緒例子

public class TestRunnable implements Runnable {

    private final String name;

    public TestRunnable(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new TestRunnable("李四"));
        Thread thread2 = new Thread(new TestRunnable("張三"));
        thread.start();
        thread2.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
           System.out.println(Thread.currentThread().getName()+":"+name + ": " + i);
        }
    }
}複製程式碼

執行結果如下:

Thread-1:張三: 0
Thread-1:張三: 1
Thread-1:張三: 2
Thread-1:張三: 3
Thread-1:張三: 4
Thread-0:李四: 0
Thread-1:張三: 5
Thread-0:李四: 1
Thread-1:張三: 6
Thread-1:張三: 7
Thread-0:李四: 2
...複製程式碼

2.擴充套件 Thread 類實現的多執行緒例子

public class TestThread extends Thread {

    private final String name;

    public TestThread(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Thread thread = new TestThread("李四");
        Thread thread2 = new TestThread("張三");
        thread.start();
        thread2.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + name + ": " + i);
        }
    }
}複製程式碼

執行結果如下:

Thread-0:李四: 0
Thread-0:李四: 1
Thread-0:李四: 2
Thread-0:李四: 3
Thread-0:李四: 4
Thread-0:李四: 5
Thread-0:李四: 6
Thread-0:李四: 7
Thread-0:李四: 8
Thread-0:李四: 9
Thread-0:李四: 10
Thread-0:李四: 11
Thread-1:張三: 0
Thread-0:李四: 12
Thread-0:李四: 13
...複製程式碼

對於上面的多執行緒程式碼來說,輸出的結果是不確定的。其中的 for 迴圈只是用來模擬一個耗時操作。

一些常見問題

1.執行緒的名字,一個執行中的執行緒總是有名字的,名字有兩個來源,一個是虛擬機器自己給的名字,一個是你自己定的名字。在沒有指定執行緒名字的情況下,虛擬機器總會為執行緒指定名字,並且主執行緒的名字總是 main,非主執行緒的名字不確定。
2.執行緒都可以設定名字,也可以獲取名字,主執行緒也不例外。
3.獲取當前執行緒的物件的方法是:Thread.currentThread();
4.在上面的程式碼中,只能保證:每個執行緒都將啟動,每個執行緒都將執行直到完成。一系列執行緒以某種順序啟動並不意味著將按該順序執行。對於任何一組啟動的執行緒來說,排程程式不能保證其執行次序,持續時間也無法保證。
5.當執行緒目標 run 方法結束時該執行緒結束。
6.一旦執行緒啟動,它就永遠不能再重新啟動。
7.執行緒的排程是 JVM 的一部分,在一個 CPU 的機器上,實際上一次只能執行一個執行緒。一次只能有一個執行緒棧執行。JVM 執行緒排程程式決定實際執行哪個處於可執行狀態的執行緒。眾多可執行執行緒中的某一個會被選中做為當前執行緒。可執行執行緒被選擇執行的順序是沒有保障的。
8.儘管通常採用佇列形式,但這是沒有保障的。佇列形式是指當一個執行緒完成“一輪”時,它移動到可執行佇列的尾部等待,直到它最終排隊到該佇列的前端為止,它才能被再次選中。事實上,我們把它稱為可執行池而不是一個可執行佇列,目的是幫助認識執行緒並不都是以某種有保障的順序執行的一個佇列例項。
9.儘管我們無法控制執行緒排程程式,但可以通過別的方式來影響執行緒排程方式。

Java 執行緒:執行緒棧模型

要理解執行緒排程的原理,以及執行緒執行過程,必須理解執行緒模型。
執行緒棧是指某時刻記憶體中執行緒排程的棧資訊,當前呼叫的方法總是位於棧頂。執行緒棧的內容是隨著程式的執行動態變化的,因此研究執行緒棧必須選擇一個執行的時刻。

下面通過一個示例的程式碼說明執行緒棧的變化過程。

這幅圖描述在程式碼執行到兩個不同時刻,虛擬機器呼叫棧示意圖。
當程式執行到thread.start()的時候,程式多出一個分支(增加了一個呼叫棧 B),這樣,棧 A、棧 B 並行執行。

Java 執行緒:執行緒狀態的轉換

執行緒狀態

執行緒的狀態轉換是執行緒控制的基礎。執行緒狀態總得來說可分為五種:分別是建立、銷燬、可執行(就緒)、執行、等待/阻塞。用一張圖描述如下:

我懶得畫圖了,從度娘那裡偷了一張圖,忽略“死忙”~

1.建立:執行緒物件已經建立,還沒有呼叫 start 方法。
2.可執行:當現場有資格執行,但排程程式還沒有把它選的為執行時執行緒所處的狀態。當 start 方法呼叫時,執行緒首先進入可執行狀態。線上程執行之後或者從阻塞、等待或睡眠狀態回來後,也返回到可執行狀態。
3.執行:執行緒排程程式從可執行池中選擇一個執行緒作為執行狀態。這也是執行緒進入執行狀態的唯一一種方式。
4.等待/阻塞/睡眠:這是執行緒有資格執行時它所處的狀態。實際上這個狀態組合為一種,其共同點是:執行緒仍然是活動,但是不具備執行條件,在滿足了一定條件之後會回到可執行狀態。
5.銷燬:當現場的 run 方法完成時,該執行緒就會被銷燬,也許執行緒物件還是活的,但是它已經不是一個單獨執行的執行緒。如果再次呼叫 start 方法,會丟擲 IllegalThreadStateException 異常。

組織執行緒執行

對於執行緒的阻止,考慮一下三個方面(不考慮 I/O阻塞)

  • 睡眠
  • 等待
  • 因為需要一個物件的鎖定而被阻塞

睡眠

Thread.sleep()靜態方法強制當前正在執行的執行緒休眠(暫停執行),以“減慢執行緒”。當執行緒睡眠時,它入睡在某個地方,在甦醒前不會返回到可執行狀態。當睡眠時間到期,則返回到可執行狀態。

執行緒睡眠的原因:執行緒執行太快,或者需要強制進入下一輪,因為 Java 規範不保證合理的輪換。

睡眠的實現:呼叫靜態方法

try{
    Thread.sleep(100);
}catch(Exception e){
}複製程式碼

睡眠的位置,為了讓其他執行緒有機會執行,可以將 Thread.sleep()的呼叫放執行緒 run方法之內,這樣才能保證該執行緒執行過程中會睡眠。

注意:

  • 執行緒睡眠是幫助所有執行緒活得執行機會的最好方法。
  • 執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。
  • sleep 是靜態方法,只能控制當前正在執行的執行緒。

執行緒的優先順序和執行緒讓步 yield()

執行緒的讓步是通過 Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。

要理解 yield,必須瞭解執行緒的優先順序的概念。執行緒總是存在優先順序,優先順序範圍在1~10之間。JVM 執行緒排程程式是基於優先順序的搶先排程機制。在大多數情況下,下一個選擇為執行的執行緒優先順序將大於或等於執行緒池中任何執行緒的優先順序。

注意:當設計多執行緒應用程式的時候,一定不要依賴於執行緒的優先順序。因為執行緒排程優先順序操作是沒有保障的,只能把執行緒優先順序作用為一種提高程式效率的方法,但是要保證程式不依賴這種操作。

當執行緒池中執行緒都具有相同的優先順序,排程的 JVM 實現自由選擇它喜歡的執行緒。這時候排程程式的操作有兩種可能:一是選擇一個執行緒執行,直到它阻塞或者執行完成為止。二是時間分片,為池內的每個執行緒提供均等的執行機會。

設定執行緒的優先順序:執行緒預設的優先順序是建立她的執行執行緒的優先順序。可以通過 setPriority(int newPropority)更改執行緒的優先順序。例如:

Thread t = new Thread();
t.setPriority(8);
t.start();複製程式碼

執行緒優先順序為1~10直接的正正式,JVM 從不會改變一個執行緒的優先順序。然而1~10直接的值是沒有保證的。一些 JVM 可能不能識別10個不同的值,而將這些優先順序進行每兩個或多個合併,變成少於10個的優先順序,則兩個或多個優先順序的執行緒可能被對映成為一個優先順序。

執行緒預設優先順序是5,Thread 類中有三個常量,定義執行緒優先順序範圍:

/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;
/**
 * The default priority that is assigned to a thread.
 */
public final static int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public final static int MAX_PRIORITY = 10;複製程式碼

Thread.yield() 方法

Thread.yield() 方法作用是:暫停當前正在實行的執行緒,並將其變成可執行執行緒,再從可執行執行緒池裡面隨機選擇一個執行緒作為執行執行緒。

join 方法

Thread的非靜態方法join()讓一個執行緒B“加入”到另外一個執行緒A的尾部。在A執行完畢之前,B不能工作

如以下程式碼,線上程 t 執行結束之前,int i 是不會被執行到的。

Thread t = new MyThread();
t.start();
t.join();
int i = 0;複製程式碼

小結

到目前為止,介紹了執行緒離開執行狀態的3種方法:
1.呼叫 Thread.sleep()方法
2.呼叫 Thread.yield()方法
3.呼叫 join 方法。

除了以上三種外,還有下面幾種特殊情況可能使執行緒離開執行狀態:
1.執行緒 run 方法結束
2.呼叫 Object 的 wait 方法。
3.執行緒不能在物件上獲得鎖定,它正試圖執行該物件的方法程式碼
3.執行緒排程程式可以決定將當前執行狀態移動到可執行狀態,以便讓另一個執行緒獲得執行機會,不需要任何理由。

Java 執行緒:執行緒的同步與鎖

同步問題提出

執行緒的同步是為了防止多個執行緒訪問一個資料物件時,對資料早餐的破壞。
例如:兩個執行緒 A、B 都操作同一個物件,並修改物件上的資料。

懶得貼程式碼,我直接貼圖了。

我們的期望值 i 是不能出現小於0的情況的,但是由於多執行緒的操作,出現了-1.

同步和鎖定

鎖的原理

Java 中每個物件都有一個內建鎖

當程式執行到非靜態的 synchronized 同步方法上時,自動獲得與之正在執行程式碼類的當前實力(this 實力)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。

當程式執行到 synchronized 同步方法或者程式碼塊時候,才對該物件鎖起作用。

一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,知道第一個執行緒釋放鎖。這意味著其他任何執行緒都不能進入到該物件上的 synchronized 方法或程式碼塊,直到該鎖被釋放。

鎖釋放是指持鎖的執行緒退出了 synchronized 同步程式碼塊。

關於鎖和同步,有以下幾個點需要注意:
1.只能同步方法,不能同步變數和類;
2.每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步。
3.不必同步的類中所有的方法,類可以同時擁有同步和非同步的方法。
4.如果兩個執行緒要執行同一個類中的 synchronized 方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠獲取到鎖並執行方法,另外一個執行緒需要等待,直到鎖被釋放。
5.如果執行緒擁有同步和非同步方法,則非同步方法沒有訪問限制。
6.執行緒睡眠時,它所持有的任何鎖都不會被釋放
7.執行緒可以獲得多個鎖。但是要注意避免死鎖
8.同步損害併發性,應該儘可能縮小同步範圍。同步不必同步整個方法,可以只同步方法中的幾句程式碼。
9.在方法上加 synchronized 和在方法裡面加 synchronized 程式碼塊把所有的直接語句都包裹的效果是一樣的。

靜態方法同步

由於靜態方法隨著類的載入而載入的,先於物件而存在,所以靜態方法同步用的鎖是這個類的 class。

public static synchronized int setName(String name){
      Xxx.name = name;
}
等價於
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}複製程式碼

如果執行緒不能獲得鎖會怎樣

如果執行緒檢視進入同步方法,而鎖已經被佔用,則該執行緒在該物件上被阻塞。實質上,執行緒進入該物件的一種池中,必須在那裡等待,知道鎖被釋放,該執行緒再次變為可執行或執行為止。

當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1.呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干擾。
2.呼叫同一類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的 Class 物件上。
3.靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法所在 Class 物件上,非靜態方法鎖定在該類的物件上。
4.對於同步程式碼塊,要看清楚什麼物件已經用於鎖定。在同一個物件進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。

何時需要同步

在多個執行緒同時訪問互斥(可互動)資料時,應該同步以保護資料,確保兩個執行緒不會同時修改它。

對於非靜態欄位可更改的資料,通常使用非靜態方法訪問。
對於靜態欄位可更改的資料,通常使用靜態方法訪問。

執行緒安全類

當一個類以及很好的同步以保護它的資料時,這個類就稱為“執行緒安全的”。

即使是執行緒安全類,也要特別小心,因為操作的執行緒間仍然不一定安全。

舉個例子,我們都知道 ArrayLis 不是執行緒安全的類,但是 Collections.synchronizedList()這個工具類裝飾一下,就變成執行緒安全的了,如下:

ArrayList<String> arrayList = new ArrayList<>();
List<String> list = Collections.synchronizedList(arrayList);複製程式碼

此時 list 是一個執行緒安全類了對吧,現在多個執行緒執行以下語句

if (list.size()!=0){
    list.remove(0);
}複製程式碼

是不是同樣會出現問題。哈哈哈哈~此題答案不唯一,看到這裡的童鞋不會處理這個問題的話,回頭再把這篇文章重新看一遍吧。

執行緒死鎖

死鎖對於 Java 程式來說,是很複雜的,也很難排查。正常情況下,我們故意寫出死鎖程式碼,也很難出現一次問題。

public class DeadlockRisk {
    private static class Resource {
        publicint value;
    } 

    private Resource resourceA =new Resource();
    private Resource resourceB =new Resource();

    publicint read() {
        synchronized (resourceA) {
               Thread.sleep(50);
            synchronized (resourceB) {
                return resourceB.value + resourceA.value;
            } 
        } 
    } 

    publicvoid write(int a,int b) { 
        synchronized (resourceB) {
                Thread.sleep(50);
            synchronized (resourceA) {
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}複製程式碼

為了讓死鎖必現,我讓執行緒睡眠了50ms,當兩個不同的執行緒同時 分別訪問 read 方法和 write 方法時,程式就 GG 了。

執行緒同步小結

1.執行緒同步的目的是為了保護多個執行緒訪問一個資源時對資源的破壞。
2.執行緒同步方法是通過鎖來實現,每個物件都有且僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他方法該物件的執行緒就無法再訪問物件的其他同步方法。
3.對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的 Class 物件。靜態和非靜態方法的鎖互不干擾。一個執行緒活得鎖,當在一個同步方法中訪問另外物件上的同步方法時,或獲取這兩個鎖。
4.對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5.編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全作出正確的判斷,對“原子”操作做出分析,並保證院子操作期間別的執行緒無法訪問競爭資源。
6.當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將傳送阻塞。
7.死鎖是執行緒間相互等待鎖造成的,在實際中傳送的概率非常小。真寫個死鎖,不一定好使,但是一旦發生死鎖,程式就 GG 了。

相關文章