首頁 > 軟體

Java多執行緒Thread類的使用及注意事項

2022-06-20 14:01:22

Thread類的基本用法

建立子類,繼承自Thread並且重寫run方法:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}
public class Demo1 {
    public static void main(String[] args) {
        // 最基本的建立執行緒的辦法.
        Thread t = new MyThread();
        //呼叫了start方法才是真正的在系統中建立了執行緒,執行run方法
        t.start();
    }
}

建立一個類,實現Runnable介面再建立Runnable是範例傳給Thread

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello");
    }
}
public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

匿名內部類:

建立了一個匿名內部類,繼承自Thread類,同時重寫run方法,再new出匿名內部類的範例

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello");
            }
        };
        t.start();
    }
}

new的Runnable,針對這個建立的匿名內部類,同時new出的Runnable範例傳給Thread的構造方法

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        t.start();
    }
}

lambda表示式 lambda代替Runnable

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("hello");
        });
        t.start();
    }
}

執行緒指標

  • 1.isDaemon();是否後臺執行緒 後臺執行緒不影響程序退出,不是後臺執行緒會影響程序退出
  • 2.isAlive();是否存活 在呼叫start前系統中是沒有對應執行緒的,run方法執行完後執行緒就銷燬了,t物件可能還存在
  • 3.isinterrupted();是否被中斷

run和start的區別:run單純的只是一個普通方法描述了任務的內容 start則是一個特殊的方法,內部會在系統中建立執行緒

中斷執行緒

執行緒停下來的關鍵是要讓對應run方法執行完,對於main執行緒來說main方法執行完了才會終止

1.手動設定標誌位

線上程中控制這個標誌位就能影響到這個執行緒結束,但是此處多個執行緒共用一片虛擬空間,因此main執行緒修改的isQuit和t執行緒判斷的isQuit是同一個值

public class Demo10 {
    // 通過這個變數來控制執行緒是否結束.
    private static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 就可以在 main 執行緒中通過修改 isQuit 的值, 來影響到執行緒是否退出
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // main 執行緒在 5s 之後, 修改 isQuit 的狀態.
        isQuit = true;
    }
}

2.使用Thread中內建的一個標誌位來判定

Thread.interruted()這是一個靜態方法 Thread.currentThread().isInterrupted()這是一個實體方法,其中currentThread能夠獲取到當前執行緒的範例

public class Demo7 {
    public static void main(String[] args)  {
        Thread t = new Thread(() -> {
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 當觸發異常之後, 立即就退出迴圈~
                    System.out.println("這是收尾工作");
                    break;
                }
            }
        });
        t.start();
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        // 在主執行緒中, 呼叫 interrupt 方法, 來中斷這個執行緒.
        // t.interrupt 的意思就是讓 t 執行緒被中斷!!
        t.interrupt();
    }
}

需要注意的是呼叫這個方法t.interrupt()可能會產生兩種情況:

  • 1)如果t執行緒處在就緒就設定執行緒的標誌位為true
  • 2)如果t執行緒處在阻塞狀態(sleep),就會觸發一個InterruptExeception

執行緒等待

多個執行緒之間排程順序是不確定的,有時候我們需要控制執行緒之間的順序,執行緒等待就是一種控制執行緒執行順序的手段,此處的執行緒等待只要是控制執行緒結束的先後順序。
哪個執行緒中的join,哪個執行緒就會阻塞等待直到對應的執行緒執行完畢為止。

  • t.join();呼叫這個方法的執行緒是main執行緒,針對t這個物件呼叫的此時就是讓main等待t。程式碼執行到join這一行就停下了,讓t先結束然後main繼續。
  • t.join(10000);join提供了另一個版本為帶一個引數的,引數為等待時間10s之後join直接返回不再等待

Thread.currentThread()

能夠獲取當前執行緒的應用,哪個執行緒呼叫的currentThread就獲取到哪個執行緒的範例 對比this如下:

對於這個程式碼來說,通過繼承Thread的方法來建立執行緒。此時run方法中直接通過this拿到的就是當前Thread的範例

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                System.out.println(this.getName());
            }
        };
        t.start();
    }
}

然而此處this不是指向Thread型別,而是指向Runnable,Runnable只是一個單純的任務沒有name屬性,要想拿到執行緒名字只能通過Thread.currentThread()

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                //err
                //System.out.println(this.getName());
                //right
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
    }
}

程序狀態

針對系統層面:

  • 就緒
  • 阻塞

java中Thread類進一步細化:

  • NEW:把Thread物件建立好了但是還沒有呼叫start
  • TERMINATED:作業系統中的執行緒已執行完畢銷燬,但是Thread物件還在獲取到的狀態
  • RUNNABLE:就緒狀態,處在該狀態的執行緒就是在就緒佇列中,隨時可以排程到CPU上
  • TIME_WAITING:呼叫了sleep就會進入到該狀態,join(超時時間) BLOCKED:當前執行緒在等待鎖導致了阻塞
  • WAITING:當前執行緒在等待喚醒

狀態轉換圖:

執行緒安全問題

定義:作業系統中執行緒排程是隨機的,導致程式的執行可能會出現一些bug。如果因為排程隨機性引入了bug執行緒就是不安全的,反之則是安全的。
解決方法:加鎖,給方法直接加上synchronized關鍵字,此時進入方法就會自動加鎖,離開方法就會自動解鎖。當一個執行緒加鎖成功的時候,其他執行緒嘗試加鎖就會觸發阻塞等待,阻塞會一直持續到佔用鎖的執行緒把鎖釋放為止。

synchronized public void increase() {
        count++;
}

執行緒不安全產生的原因:

  • 1.執行緒是搶佔式執行,執行緒間的排程充滿隨機性。
  • 2.多個執行緒對同一個變數進行修改操作
  • 3.針對變數的操作不是原子的
  • 4.記憶體可見性也會影響執行緒安全(針對同一個變數t1執行緒迴圈進行多次讀操作,t2執行緒少次修改操作,t1就不會從記憶體讀資料了而是從暫存器裡讀)
  • 5.指令重排序,也是編譯器優化的一種操作,保證邏輯不變的情況下調整順序,解決方法synchronized。

記憶體可見性解決方法:

  • 1.使用synchronized關鍵字 使用synchronized不光能保證指令的原子性,同時也能保證記憶體的可見性。被synchronized包裹起來的程式碼編譯器就不會從暫存器裡讀。
  • 2.使用volatile關鍵字 能夠保證記憶體可見性,禁止編譯器作出上述優化,編譯器每次執行判定相等都會重新從記憶體讀取。

synchronized用法

在java中每個類都是繼承自Object,每個new出來的範例裡面一方面包含自己安排的屬性,另一方面包含了“物件頭”即物件的一些後設資料。加鎖操作就是在這個物件頭裡面設定一個標誌位。

1.直接修飾普通的方法

使用synchronized的時候本質上是對某個“物件”進行加鎖,此時的鎖物件就是this。加鎖操作就是在設定this的物件頭的標誌位,當兩個執行緒同時嘗試對同一個物件加鎖的時候才有競爭,如果是兩個執行緒在針對兩個不同物件加鎖就沒有競爭。

class Counter{
	public int count;
	synchronized public void increase(){
	count++;
	}
}

2.修飾一個程式碼塊

需要顯示制定針對那個物件加鎖(java中的任意物件都可以作為鎖物件)

public void increase(){
    synchronized(this){
    count++;
    }
}

3.修飾一個靜態方法

相當於針對當前類的類物件加鎖,類物件就是執行程式的時候。class檔案被載入到JVM記憶體中的模樣。

synchronized public static void func(){
}

或者

public static void func(){
    synchronized(Counter.class){

    }
}

監視器鎖monitor lock

可重入鎖就是同一個執行緒針對同一個鎖,連續加鎖兩次,如果出現死鎖就是不可重入鎖,如果不會死鎖就是可重入的。因此就把synchronized實現為可重入鎖,下面的例子裡啊連續加鎖操作不會導致死鎖。可重入鎖內部會記錄所被哪個執行緒佔用也會記錄加鎖次數,因此後續再加鎖就不是真的加鎖而是單純地把技術給自增。

synchronized public void increase(){
    synchronized(this){
        count++;
    }
}

死鎖的其他場景

  • 1.一個執行緒一把鎖
  • 2.兩個執行緒兩把鎖
  • 3.N個執行緒M把鎖(哲學家就餐問題,解決方法:先拿編號小的筷子)

死鎖的四個必要條件(前三個都是鎖本身的特點)

  • 1.互斥使用,一個鎖被另一個執行緒佔用後其他執行緒就用不了(鎖的本質,保證原子性)
  • 2.不可搶佔,一個鎖被一個執行緒佔用後其他執行緒不可把這個鎖給挖走
  • 3.請求和保持,當一個執行緒佔據了多把鎖之後,除非顯示的釋放否則這些鎖中都是該執行緒持有的
  • 4.環路等待,等待關係成環(解決:遵循固定的順序加鎖就不會出現環路等待)

java執行緒類:

  • 不安全的:ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder
  • 安全的:Vector,HashTable,ConcurrentHashMap,StringBuffer,String

volatile

禁止編譯器優化保證記憶體可見性,產生原因:計算機想執行一些計算就需要把記憶體的資料讀到CPU暫存器中,然後再從暫存器中計算寫回到記憶體中,因為CPU存取暫存器的速度比存取記憶體快很多,當CPU連續多次存取記憶體結果都一樣,CPU就會選擇存取暫存器。

JMM(Java Memory Model)Java記憶體模型

就是把硬體結構在java中用專業的術語又重新抽象封裝了一遍。

  • 工作記憶體(work memory)其實指的不是記憶體,而是CPU暫存器。
  • 主記憶體(main memeory)這才是主記憶體。
  • 原因:java作為一個跨平臺程式語言要把硬體細節封裝起來,假設某個計算機沒有CPU或者記憶體同樣可以套到上述模型中。

暫存器,快取和記憶體之間的關係

CPU從記憶體取資料太慢,因此把資料直接放到暫存器裡來讀,但暫存器空間太緊張於是又搞了一個儲存空間,比暫存器大比記憶體小速度比暫存器慢比記憶體快稱為快取。暫存器和快取統稱為工作記憶體。

暫存器,快取和記憶體之間的關係圖

  • 儲存空間:CPU<L1<L2<L3<記憶體
  • 速度:CPU>L1>L2>L3>記憶體
  • 成本:CPU>L1>L2>L3>記憶體

volatile和synchronized的區別

  • volatile只是保證可見性不保證原子性,只是處理一個執行緒讀和一個執行緒寫的過程。
  • synchronized都能處理

wait和notify

等待和通知處理執行緒排程隨機性問題的,join也是一種控制順序的方式更傾向於控制執行緒結束。wait和notify都是Object物件的方法,呼叫wait方法的執行緒就會陷入阻塞,阻塞到有執行緒通過notify來通知。

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait前");
        object.wait();
        System.out.println("wait後");
    }
}

wait內部會做三件事;

  • 1.先釋放鎖
  • 2.等待其他執行緒的通知
  • 3.收到通知後重新獲得鎖並繼續往下執行

因此想用wait/notify就得搭配synchronized

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){
            System.out.println("wait前");
            object.wait();
            System.out.println("wait後");
        }
    }
}

注意:wait notify都是針對同一物件來操作的,例如現在有一個物件o,有10個執行緒都呼叫了o.wait,此時10個執行緒都是阻塞狀態。如果呼叫了o.notify就會把10個執行緒中的一個執行緒喚醒。而notifyAll就會把所有10個執行緒全都給喚醒,此時就會競爭鎖。

到此這篇關於Java多執行緒Thread類的使用及注意事項的文章就介紹到這了,更多相關Java Thread 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com