天天看點

四.多線程使用及其通信

并發複習筆記之第四章(多線程使用及其通信)

想看後續請持續關注

以下來源有書籍 深入了解 JVM 虛拟機,java 并發程式設計的藝術,深入淺出多線程,阿裡巴巴技術手冊以及一些公衆号 CS-Notes,JavaGuide,以及一些大廠高頻面試題吐血總結,以及狂神說視訊筆記,目的在于通過問題來複習整個多線程,已下是全部章節,覺得不錯點個贊評論收藏三連一下,您的鼓勵就是我繼續創作的最大動力!!!!

文章目錄

    • 4.多線程的使用
      • 4.1 多線程的四種建立方法
        • 4.1.1 實作接口和直接繼承Thread類哪個好?
        • 4.1.2 Runnable實作和 Callable 實作有什麼差別
      • 4.2 線程間的通信
        • 4.2.1 線程通信之 Join()
        • 4.2.2 線程通信之wait()/notify()/notifyAll
          • 4.2.2.1 wait 為什麼會釋放鎖
          • 4.2.2.2 經典的等待通知範例
          • 4.2.2.3 虛假喚醒是什麼,如何避免虛假喚醒?
          • 4.2.2.4 使用 notify 注意?
        • 4.2.3 簡單介紹下 ThreadLocal

4.多線程的使用

4.1 多線程的四種建立方法

  1. Runnable 接口
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // ...
    }
}
           

需要實作接口中的 run() 方法。

public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
           

使用 Runnable 執行個體再建立一個 Thread 執行個體,然後調用 Thread 執行個體的 start() 方法來啟動線程。

  1. 實作 Callable 接口
public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
           
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
           

一個類實作 callable 接口 -> 建立一個執行個體 送入 ->FutureTask implements RunnableFuture extends Runnable 放入 Thread 内部

  1. 繼承 Thread 類
public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
           
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
           

同樣也是需要實作 run() 方法,因為 Thread 類也實作了 Runable 接口。

當調用 start() 方法啟動一個線程時,虛拟機會将該線程放入就緒隊列中等待被排程,當一個線程被排程時會執行該線程的 run() 方法。

  1. 線程池的方法

轉線程池章節

4.1.1 實作接口和直接繼承Thread類哪個好?

  1. 實作接口好,因為 java 隻有單繼承,如果繼承了之後就無法繼承其它類,但可以實作多個接口
  2. 類可能隻要求可執行就行,繼承整個 Thread 類開銷過大。

4.1.2 Runnable實作和 Callable 實作有什麼差別

  1. Runnable執行方法是run(),Callable是call()
  2. 實作Runnable接口的任務線程無傳回值;實作Callable接口的任務線程能傳回執行結果
  3. call方法可以抛出異常,run方法若有異常隻能在内部消化

4.2 線程間的通信

4.2.1 線程通信之 Join()

如果一個線程A執行了thread.join()語句,其含義是:目前線程A等待thread線程終止之後才 從thread.join()傳回。

public class Juc {

    public static void main(String[] args) throws Exception {
            Thread previous = Thread.currentThread();
            for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Domino(previous), String.valueOf(i)); 
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5); 
        System.out.println(Thread.currentThread().getName() + " terminate.");
    } 
    static class Domino implements Runnable {
        private Thread thread; public Domino(Thread thread) {
            this.thread = thread; 
        } 
        public void run() {
            try { 
                thread.join();
            } catch (InterruptedException e) {
                
            } 
            System.out.println(Thread.currentThread().getName() + " terminate."); 
        }
    }
}
           

每個線程終止的前提是前驅線程的終止,每個線程等待前驅線程 終止後,才從join()方法傳回,這裡涉及了等待/通知機制(等待前驅線程結束,接收前驅線程結 束通知)。

我們可以看 Join()源碼

public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }
           

由源碼我們可以看出,調用 join 方法的線程死去之後,才會從 join()方法處傳回

4.2.2 線程通信之wait()/notify()/notifyAll

方法 描述
wait() 調用該方法進入 wating 狀态,需要被通知或者中斷後會傳回,注意 wait 方法會釋放鎖
notify() 通知一個在對象等待的線程,從 wait() 方法傳回,特别注意不會立刻釋放鎖,執行完代碼塊才會釋放
notifyAll() 通知所有等待隊列的線程
wait() 逾時等待一會,如果逾時了,就傳回

​ 在圖中,WaitThread首先擷取了對象的鎖,然後調用對象的wait()方法,進而放棄了鎖 并進入了對象的等待隊列WaitQueue中,進入等待狀态。由于WaitThread釋放了對象的鎖, NotifyThread随後擷取了對象的鎖,并調用對象的notify()方法,将WaitThread從WaitQueue移到 SynchronizedQueue中,此時WaitThread的狀态變為阻塞狀态。NotifyThread釋放了鎖之後, WaitThread再次擷取到鎖并從wait()方法傳回繼續執行。

4.2.2.1 wait 為什麼會釋放鎖

這是因為,如果沒有釋放鎖,那麼其它線程就無法進入對象的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒挂起的線程,造成死鎖。

4.2.2.2 經典的等待通知範例
synchronized(對象) {
		while(條件不滿足) { 
		對象.wait();
	}
	對應的處理邏輯 
}

synchronized(對象) {
  改變條件 對象.notifyAll(); 
}
           
4.2.2.3 虛假喚醒是什麼,如何避免虛假喚醒?

虛假喚醒:線上程的 等待/喚醒 的過程中,等待的線程被喚醒後,在條件不滿足的情況依然繼續向下運作了。

為什麼使用 if()會出現虛假喚醒

  1. 線程被喚醒後,會從 wait() 處開始繼續往下執行;
  2. while 被掉換成 if 後出現了虛假喚醒,出現了我們不想要的結果;
  3. 因為if隻會執行一次,執行完會接着向下執行if()外邊的而while不會,直到條件滿足才會向下執行while()外邊的
4.2.2.4 使用 notify 注意?
  1. notify()或notifyAll()方法調用後,等待線程依舊不會從wait()傳回,需要調用notify()或 notifAll()的線程釋放鎖之後,等待線程才有機會從wait()傳回。注意與 wait 方法

4.2.3 簡單介紹下 ThreadLocal

通常情況下,我們建立的變量是可以被任何一個線程通路并修改的。如果想實作每一個線程都有自己的專屬本地變量該如何解決呢? JDK 中提供的

ThreadLocal

類正是為了解決這樣的問題。

ThreadLocal

類主要解決的就是讓每個線程綁定自己的值,可以将

ThreadLocal

類形象的比喻成存放資料的盒子,盒子中可以存儲每個線程的私有資料。

如果你建立了一個

ThreadLocal

變量,那麼通路這個變量的每個線程都會有這個變量的本地副本,這也是

ThreadLocal

變量名的由來。他們可以使用

get()

set()

方法來擷取預設值或将其值更改為目前線程所存的副本的值,進而避免了線程安全問題。