java 多執行緒

yimgg發表於2020-11-29

很多人可能已經很熟悉作業系統中的多工:就是同一時刻執行多個程式的能力。

多執行緒程式在較低層次上擴充套件了多工的概念:一個程式同時執行多個任務。通常每一個任務稱為一個執行緒,它是執行緒控制的簡稱。可以同時執行一個以上執行緒的程式成為多執行緒程式。

什麼是執行緒

執行緒

執行緒是程式的一個實體,是CPU排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位。執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程式的其他的執行緒共享程式所擁有的全部資源。

多執行緒

多執行緒指在單個程式中可以同時執行多個不同的執行緒執行不同的任務。

多執行緒程式設計的目的,就是“最大限度地利用cpu資源”,當某一執行緒的處理不需要佔用cpu而只和io等資源打交道時,讓需要佔用Cpu的其他縣城有其他機會獲得cpu資源。從根本上說,這就是多執行緒程式設計的最終目的。

有一個程式實現多個程式碼同時交替執行就需要產生多個執行緒。CPU隨機地抽出時間。讓我們程式一會做這件事,一會做另外的事情。

Java內建支援多執行緒程式設計(Multithreaded Programming)。

多執行緒程式包含兩條及以上的併發執行的部分,程式中每個這樣的部分都叫做一個執行緒(Thread)。每個執行緒都有獨立的執行路徑,因此多執行緒是多工處理的特殊形式。

多工處理被所有的現代作業系統所支援。然而,多工處理有兩種截然不同的型別:基於程式的基於執行緒的

  1.基於程式的多工處理是更熟悉的形式。程式(process)本質上是一個執行的程式。因此基於程式的多工處理的特點是允許你的計算機同時執行兩個或更多的程式。

  比如,基於程式的多工處理是你在編輯文字的時候可以同時執行Java編譯器。

  2.而在基於執行緒(thread-based)的多工處理環境中,執行緒是最小的執行單位。這意味著一個程式可以同時執行兩個或者多個任務的功能。

  比如,一個文字編輯器可以在列印的同時格式化文字。

執行緒和程式的區別

 多個程式的內部資料和狀態都是完全獨立的,而多執行緒是共享一塊記憶體空間和一組系統資源,有可能互相影響。

  執行緒本身的資料通常只有暫存器資料,以及一個程式執行時使用的堆疊,所以執行緒的切換負擔比程式切換的負擔要小。多執行緒程式比多程式程式需要更少的管理費用。

  程式是重量級的任務,需要分配給它們獨立的地址空間,程式間通訊是昂貴和受限的,程式間的轉換也是很需要花費的。而執行緒是輕量級的選手,它們共享相同的地址空間並且共同分享同一個程式,執行緒間的通訊是便宜的,執行緒間的轉換也是低成本的。

執行緒的實現

在Java中通過run方法為執行緒指明要完成的任務,有兩種技術來為執行緒提供run方法:

  1. 繼承Thread類並重寫它的run方法。之後建立這個子類的物件並呼叫start()方法。
  2. 通過定義實現Runnable介面的類進而實現run方法。這個類的物件在建立Thread的時候作為引數被傳入,然後呼叫start()方法。

兩種方法需要執行執行緒start()方法為執行緒分配必須的系統資源,排程執行緒執行並紙呢個執行緒的run()方法。

start()方法是啟動執行緒的唯一的方法。start()方法首先為執行緒的執行準備好系統資源,然後再去呼叫run()方法。一個執行緒只能啟動一次,再次啟動就不合法了。

run()方法中放入了執行緒的邏輯,即我們要這個執行緒去做事情。

通常我一般是使用第二種方法來實現的,當一個執行緒已經繼承了另一個類時,只能用第二種方法來構造,即實現Runnable介面。

Thread

package com.java.test;

public class ThreadTest
{
    public static void main(String[] args)
    {
        TreadTest1 thread1 = new TreadTest1();
        TreadTest2 thread2 = new TreadTest2();

        thread1.start();
        thread2.start();
    }

}
class TreadTest1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; ++i)
        {
            System.out.println("Test1 " + i);
        }
    }
}
class TreadTest2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; ++i)
        {
            System.out.println("Test2 " + i);
        }
    }
}
複製程式碼

當使用第一種方式(繼承Thread的方式)來生成執行緒物件時,我們需要重寫run()方法,因為Thread類的run()方法此時什麼事情也不做。

Runnable

package com.java.test;

public class ThreadTest
{
    public static void main(String[] args)
    {
//         執行緒的另一種實現方法,也可以使用匿名的內部類
        Thread threadtest1=new Thread((new ThreadTest1()));
        threadtest1.start();
        Thread threadtest2=new Thread((new ThreadTest2()));
        threadtest2.start();
    }
}

class ThreadTest1 implements Runnable
{

    @Override
    public void run()
    {
        for (int i = 0; i < 100; ++i)
        {
            System.out.println("Hello: " + i);
        }
    }
}

class ThreadTest2 implements Runnable
{

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

  當使用第二種方式(實現Runnable介面的方式)來生成執行緒物件時,我們需要實現Runnable介面的run()方法,然後使用new Thread(new RunnableClass())來生成執行緒物件(RunnableClass已經實現了Runnable介面),這時的執行緒物件的run()方法會呼叫RunnableClass的run()方法

Runnable原始碼

package java.lang;
public
interface Runnable {
    public abstract void run();
}
複製程式碼

Runnable介面中只有run()方法,Thread類也實現了Runnable介面,因此也要實現了介面中的run()方法。

當生成一個執行緒物件時,如果沒有為其指定名字,那麼執行緒物件的名字將使用如下形式:Thread-number,該number是自動增加的數字,並被所有的Thread物件所共享,因為它是一個static的成員變數。

停止執行緒 

  現線上程的消亡不能通過呼叫stop()命令,應該讓run()方法自然結束。stop()方法是不安全的,已經廢棄。

  停止執行緒推薦的方式:設定一個標誌變數,在run()方法中是一個迴圈,由該標誌變數控制迴圈是繼續執行還是跳出;迴圈跳出,則執行緒結束。

執行緒共享資源

多執行緒程式設計很常見的情況下是希望多個執行緒共享資源,通過多個執行緒同時消費資源來提高效率,但是新手一不小心很容易陷入一個編碼誤區。

舉個小栗子(*╹▽╹*)

下面的程式碼,希望通過 3 個執行緒同時執行 i-- ,使得輸出i 的值為 0,但3 次輸出的結果都為 2。這是因為在 main 方法中建立的三個執行緒都獨自持有一個 i 變數,我們的目的一應該是 3 個執行緒共享一個 i 變數。

class ThreadTest1 extends Thread {
    private int i = 3;
    @Override
    public void run() {
        i--;
        System.out.println(i);
    }
}

public class ThreadTest {
    public static void main(String[] strings) {
        Thread thread1 = new ThreadTest1();
        thread1.start();
        Thread thread2 = new ThreadTest1();
        thread2.start();
        Thread thread3 = new ThreadTest1();
        thread3.start();
    }
}
輸出 // 2 2 2複製程式碼

應該這麼寫

public class ThreadTest {
    public static void main(String[] strings) {
        Thread thread1 = new ThreadTest1();
        thread1.start();
        thread1.start();
        thread1.start();    
    }
}
複製程式碼

多個執行緒執行同樣的程式碼

在這種情況下,可以使用同一個Runnable物件(看上一篇部落格,這是一種建立執行緒的方式)將需要共享的資料,植入這個Runnable物件裡面。例如買票系統,餘票是需要共享的,不過在這樣做的時候,我想還應該加上synchronized關鍵字修飾!

多個執行緒執行的程式碼不一樣

在這種情況下,就兩種思路可以實現(這裡參考張孝祥老師的觀點)

其一:將共享資料封裝再另外一個物件中,然後將這個物件逐一傳遞給各個Runnable物件。每個執行緒對共享資料的操作方法也分配到那個物件身上去完成,這樣容易實現對改資料進行的各個操作的互斥和通訊。

其二:將這些Runnable物件作為某一個類中的內部類,共享資料作為這個外部類中的成員變數,每個執行緒對共享資料的操作方法也分配給外部類,以便實現對共享資料進行的各個操作的互斥和通訊,作為內部類的各個Runnable物件呼叫外部類的這些方法。

組合:將共享資料封裝再另外一個物件中,每個執行緒對共享資料的操作方法也分配到那個物件身上去完成,物件作為這個外部類中的成員變數或方法中的區域性變數,每個執行緒的Runnable物件作為外部類中的成員內部類或區域性內部類。(示例程式碼所使用的方法),總之,要同步互斥的幾段程式碼最好是分別放在幾個獨立的方法中,這些方法再放在同一個類中,這樣比較好容易實現它們之間的同步互斥通訊

簡單粗暴的方式

在程式中,定義一個static變數

執行緒的生命週期

執行緒的生命週期其實就是一個執行緒從建立到消亡的過程。

java 多執行緒

執行緒可以有6種狀態

New(新建立),Runnable(可執行),Blocked(被阻塞),Waiting(等待),Timed waiting(計時等待) Terminated(被終止)。

要想確定一個執行緒的當前狀態,可呼叫getState方法。

1.建立狀態:

  當用new操作符建立一個新的執行緒物件時,如 new Thread(t),該執行緒還沒有開始執行,該執行緒處於建立狀態,程式還沒有開始執行執行緒中程式碼。處於建立狀態的執行緒只是一個空的執行緒物件,系統不為它分配資源。

2.可執行狀態

    一旦呼叫了start()方法,執行緒處於runnable狀態。一個可執行的執行緒可能正在執行頁可能沒有執行,這取決於作業系統給執行緒提供執行的時間。

    一旦一個執行緒開始執行,它不必始終保持執行。事實上,執行的執行緒被中斷,目的是為了讓其他執行緒獲得執行機會。執行緒排程的細節依賴於作業系統提供的服務。

    記住,在任何給定時刻,一個可執行的執行緒可能正在執行頁可能沒有執行,這就是為什麼將這個狀態後才能為可執行而不是執行。

3.不可執行狀態

不可執行狀態包括被阻塞或等待狀態,此時執行緒暫時不活動,不執行任何程式碼且消耗最少的資源。直到執行緒排程器重新啟用它。

    當一個執行緒試圖獲取一個內部的物件鎖,而該鎖唄其他執行緒持有,進入阻塞狀態。當所有其他執行緒釋放繁瑣,允許執行緒排程器允許執行緒持有它的時候,該執行緒將變成非阻塞狀態。

    當執行緒等待另一個執行緒通知排程器一個條件時,它自己進入等待轉態。在呼叫wait方法和join方法,就會出現這種情況。

    有幾個方法啊有一個超時引數,例如,sleep,wait,join。呼叫他們導致執行緒進入計時等待狀態。這一狀態將保持到超時期滿或者受到適當通知。

通俗一點: 

當發生下列事件時,處於執行狀態的執行緒會轉入到不可執行狀態:

  呼叫了sleep()方法;

  執行緒呼叫wait()方法等待特定條件的滿足;

  執行緒輸入/輸出阻塞。

 返回可執行狀態:

  處於睡眠狀態的執行緒在指定的時間過去後;

  如果執行緒在等待某一條件,另一個物件必須通過notify()或notifyAll()方法通知等待執行緒條件的改變;

  如果執行緒是因為輸入輸出阻塞,等待輸入輸出完成。

4.被終止狀態

    執行緒因如下兩個原因之一而被終止:

  •     因為run方法正常退出而自然消亡
  •     因為一個沒有捕獲的異常終止了run方法而消亡

執行緒的優先順序

java執行緒有優先順序的設定,高優先順序的執行緒比地優先順序的執行緒有更高的機率得到執行。

  1. 當前執行緒沒有指定優先順序時,所有執行緒都是普通優先順序。
  2. 優先順序從1到10的範圍指定。10表示最高優先順序,1表示最低優先順序,5是普通優先順序。
  3. 優先順序最高的執行緒在執行時被給予優先。但是不能保證執行緒在啟動時就進入執行狀態。
  4. 與線上程池中等待執行機會的執行緒相比,正在執行的執行緒可能總是擁有更高的優先順序。
  5. 由排程程式決定哪一個執行緒被執行。
  6. t.setPriority()用來設定執行緒的優先順序。
  7. 記住線上程開始方法被呼叫之前,執行緒的優先順序應該被設定。
  8. 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY來設定優先順序Java執行緒的優先順序是一個整數,其取值範圍是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

    public static final int MIN_PRIORITY = 1;
    public static final int NORM_PRIORITY = 5;
    public static final int MAX_PRIORITY = 10;複製程式碼

其實不然。預設的優先順序是父執行緒的優先順序。在init方法裡,

Thread parent = currentThread();  
this.priority = parent.getPriority();  複製程式碼

可以通過setPriority方法(final的,不能被子類過載)更改優先順序。優先順序不能超過1-10的取值範圍,否則丟擲IllegalArgumentException。另外如果該執行緒已經屬於一個執行緒組(ThreadGroup),該執行緒的優先順序不能超過該執行緒組的優先順序。




相關文章