第六章、Java基礎語法----多執行緒

I_am_FuGuier發表於2020-12-22

第六章、Java基礎語法----多執行緒

本人也是剛入門Java語言,可能會有一些地方出現錯誤,描述的不對。如果發現不對的地方請及時指出,好對其進行修改。這樣不僅可以讓我學到東西,也可以讓其他剛入門的朋友學習更正確的內容。

所有內容僅供參考。不具有完全的準確性!

注:關於Java的所有內容都會參考到尚矽谷在網上公開的學習視訊及其提供的PPT

推薦:https://blog.csdn.net/kwame211/article/details/78963044

一、基本概念:程式、程式、執行緒

程式(program)是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼,靜態物件。

程式(process)是程式的一次執行過程,或是正在執行的一個程式。是一個動態的過程:有它自身的產生、存在和消亡的過程。——生命週期
如:執行中的QQ,執行中的MP3播放器
程式是靜態的,程式是動態的(程式執行起來以後就成了程式)
程式作為資源分配的單位,系統在執行時會為每個程式分配不同的記憶體區域

執行緒(thread),程式可進一步細化為執行緒,是一個程式內部的一條執行路徑。
① 若一個程式同一時間並行執行多個執行緒,就是支援多執行緒的
② 執行緒作為排程和執行的單位,每個執行緒擁有獨立的執行棧和程式計數器(pc),執行緒切換的開
銷小
③ 一個程式中的多個執行緒共享相同的記憶體單元/記憶體地址空間它們從同一堆中分配物件,可以
訪問相同的變數和物件。這就使得執行緒間通訊更簡便、高效。但多個執行緒操作共享的系統資
源可能就會帶來安全的隱患。

並行與併發
並行:多個CPU同時執行多個任務。比如:多個人同時做不同的事。
併發:一個CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事。

二、執行緒的使用
(一)執行緒的建立和啟動

  • JVM允許程式執行多個執行緒,它通過java.lang.Thread類來體現。

  • Thread類的特性

    1. 每個執行緒都是通過某個特定Thread物件的run()方法來完成操作的,經常把run()方法的主體稱為執行緒體
    2. 通過該Thread物件的start()方法來啟動這個執行緒,而非直接呼叫run()
      在這裡插入圖片描述
  • 多執行緒的建立
    方式一:繼承Thread類
    1. 建立出一個繼承於Thread類的子類
    2. 重寫Thread中的run()方法 -->將此執行緒執行的操作宣告在run()中
    3. 建立Thread類的子類的物件
    4. 通過此物件呼叫start()方法

例:

//1. 建立出一個繼承於Thread類的子類
class MyThread extends Thread{

    //2. 重寫Thread中的run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

//對上邊執行緒的使用
public class ThreadTest {
    public static void main(String[] args) {
//      3. 建立Thread類的子類的物件
        MyThread t1 = new MyThread();

//      4. 通過此物件呼叫start()方法: ① 啟動當前執行緒  ② 呼叫當前執行緒的run()
        t1.start();

        //若直接通過引用掉run()方法,則run的操作不屬於新開闢的執行緒,而是在main執行緒中執行的

        //以下操作仍是在main()執行緒中執行的
        //程式入口的main方法也屬於執行緒,為程式的主執行緒
        System.out.println("hello");
    }
}

注意:

  1. 如果自己手動呼叫run()方法(直接通過引用調run()方法),那麼就只是普通方法,沒有啟動多執行緒模式。
  2. run()方法由JVM呼叫,什麼時候呼叫,執行的過程控制都有作業系統的CPU排程決定。
  3. 想要啟動多執行緒,必須通過呼叫start方法
  4. 一個執行緒物件只能呼叫一次start()方法啟動,如果重複呼叫了,則將丟擲以上的異常“IllegalThreadStateException”。

方式二:實現Runnable介面

  1. 定義子類,實現Runnable介面。
  2. 子類中重寫Runnable介面中的run方法
  3. 通過Thread類含參構造器建立執行緒物件。
  4. 將Runnable介面的子類物件作為實際引數傳遞給Thread類的構造器中。
  5. 呼叫Thread類的start方法:開啟執行緒,呼叫Runnable子類介面的run方法。

例:

// 1. 建立一個實現了Runnable介面的類
class MyRunnable implements Runnable{

    private int ticket = 100;

// 2. 實現Runnable中的抽象方法:run()
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class RunnableThreadTest {
    public static void main(String[] args) {
//      3. 建立實現類的物件
        MyRunnable myRun1 = new MyRunnable();
//      4. 將此物件作為引數傳遞到Thread類的構造器中,建立 Thread類的物件
		Thread t = new Thread(myRun1);
//      5. 通過Thread類的物件呼叫start()方法
		t.start();
		//通過匿名物件呼叫start(),將 4 和 5 和為一步
        new Thread(myRun1).start();

    }
}

兩種方式的區別和聯絡:

  • 區別:
    繼承Thread:執行緒程式碼存放Thread子類run方法中。
    實現Runnable:執行緒程式碼存在介面的子類的run方法。

  • 開發中:優先選擇:實現Runnable介面的方式
    原因:

    1. 實現的方式避免了類的單繼承性的侷限性
    2. 實現的方式更適合處理多個執行緒共享資料的情況
  • 聯絡:
    public class Thread implements Runnable --Thread類也實現了Runnable介面
    相同點:

    1. 都需要實現run()方法,將執行緒要執行的邏輯寫在run()中
    2. 都需要通過Thread類的物件呼叫start()方法執行
  • 何時需要多執行緒

    1. 程式需要同時執行兩個或多個任務。
    2. 程式需要實現一些需要等待的任務時,如使用者輸入、檔案讀寫操作、網路操作、搜尋等。
    3. 需要一些後臺執行的程式時。
  • 多執行緒程式的優點:

    1. 提高應用程式的響應。對圖形化介面更有意義,可增強使用者體驗。
    2. 提高計算機系統CPU的利用率
    3. 改善程式結構。將既長又複雜的程式分為多個執行緒,獨立執行,利於理解和修改

三、Thread類中的方法
既然都需要通過Thread類的物件執行,那Thread類中的方法就可以供我們使用。
常用的方法:

  1. void start(): 啟動執行緒,並執行物件的run()方法
  2. run(): 執行緒在被排程時執行的操作
  3. String getName(): 返回執行緒的名稱
  4. void setName(String name):設定該執行緒名稱
  5. static Thread currentThread(): 返回當前執行緒。在Thread子類中就是this,通常用於主執行緒和Runnable實現類
  6. static void yield():執行緒讓步
    暫停當前正在執行的執行緒,把執行機會讓給優先順序相同或更高的執行緒
    若佇列中沒有同優先順序的執行緒,忽略此方法
  7. join() :當某個程式執行流中呼叫其他執行緒的 join() 方法時,呼叫執行緒將被阻塞,直到 join() 方法加入的 join 執行緒執行完為止。
    低優先順序的執行緒也可以獲得執行
  8. static void sleep(long millis):(指定時間:毫秒)
    令當前活動執行緒在指定時間段內放棄對CPU控制,使其他執行緒有機會被執行,時間到後重排隊。
    丟擲InterruptedException異常
  9. stop(): 強制執行緒生命期結束,不推薦使用
  10. boolean isAlive():返回boolean,判斷執行緒是否還活著
  • 關於執行緒的優先順序:
    Java在Thread類中定義了三個常用級別:
	public static final int MIN_PRIORITY = 1;
    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;

通過這個我們可以知道,執行緒的最高階別是10級,最低是1級。
涉及優先順序的方法:
getPriority() :返回執行緒優先值
setPriority(int newPriority) :改變執行緒的優先順序

class HelloThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
            	System.out.println(i);
            }
        }

    }
    public HelloThread(String name){
        super(name);
    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        HelloThread h1 = new HelloThread("Thread:1");
//        h1.setName("執行緒一");
        //設定分執行緒的優先順序
        h1.setPriority(Thread.MAX_PRIORITY);
        h1.start();

        //給主執行緒命名
        Thread.currentThread().setName("主執行緒");
      	//給主執行緒設定優先順序
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
    }
}

因為Thread類中定義的優先順序是靜態的,我們可以直接通過類名呼叫。如果不想使用已定的級別,我們也可以直接替換成整型值(1-10);

  • 說明
    1. 執行緒建立時繼承父執行緒的優先順序
    2. 低優先順序只是獲得排程的概率低,並非一定是在高優先順序執行緒之後才被呼叫

四、執行緒的生命週期

JDK中用Thread.State類定義了執行緒的幾種狀態

一個完整的生命週期中通常要經歷如下的五種狀態

新建: 當一個Thread類或其子類的物件被宣告並建立時,新生的執行緒物件處於新建狀態
就緒:處於新建狀態的執行緒被start()後,將進入執行緒佇列等待CPU時間片,此時它已具備了執行的條件,只是沒分配到CPU資源
執行:當就緒的執行緒被排程並獲得CPU資源時,便進入執行狀態, run()方法定義了執行緒的操作和功能
阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態
死亡:執行緒完成了它的全部工作或執行緒被提前強制性地中止或出現異常導致結束

狀態之間的轉換:
在這裡插入圖片描述

五、執行緒同步

(一)為什麼要有執行緒同步
先看一下如果沒有執行緒同步會發生什麼
在沒有執行緒同步的時候,我拿著銀行卡(儲蓄卡)去取款機取錢,在一切所有操作都就差點選確認取款的時候,這個時候我媳婦(噢,我沒有)在支付寶上也要用這個卡充錢。假設有個在程式原始碼中有個取款的方法,這時我在ATM機上已經進入這個方法,由於沒有同步控制,我媳婦在手機上也進入到了這個方法。最後的結果就是我和我媳婦都取款成功了。而此時卡上的餘額是不夠我倆取款總數的,這就導致超出的錢需要銀行出,但這個卡又不是信用卡,最終就導致銀行賠錢了。現實中這種賠錢的事銀行是不可能做的。

類似的情況還有很多,為了避免這種情況就提出了執行緒同步控制。

舉個買車票的例子:

public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("視窗一:");
        w2.setName("視窗二:");
        w3.setName("視窗三:");

        w1.start();
        w2.start();
        w3.start();

    }
}

//此時的方法也是有bug的---執行緒不同步
class Window extends Thread{
    //要使用static修飾(多個視窗共享一個變數)
    public static int ticket = 20;//總票數
    @Override
    public void run() {
        while (true){
           if (ticket > 0) {
                System.out.println(getName() + ticket);
                ticket--;
            } else {
                break;
       		}
      }
   }
}

通過結果會發現票號為20的賣出去了多次或是票號為負數的情況,這就導致有三個人的票號是一樣的,現實中是不允許出現這種情況的。

  • 出現這種問題的原因:
    當多條語句在操作同一個執行緒共享資料時,一個執行緒對多條語句只執行了一部分,還沒有執行完,另一個執行緒參與進來執行。導致共享資料的錯誤。
  • 解決辦法:
    對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。

(二)如何使用執行緒同步控制

  • 方式一:通過Synchronized同步程式碼塊
    synchronized (物件){
    // 需要被同步的程式碼;
    }

說明:

  1. 操作共享資料的程式碼,即為需要被同步的程式碼。 -->不能包含程式碼多了,也不能包含程式碼少了。
  2. 共享資料:多個執行緒共同操作的變數。比如:ticket就是共享資料。
  3. 同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。

要求:多個執行緒必須要共用同一把鎖
補充:
在實現Runable介面建立多執行緒方式中,可以考慮使用this充當同步監視器(保證this唯一)
在繼承Thread類建立多執行緒方式中,不建議使用this充當同步監視器,可以考慮當前類充當同步監視器

例:
還用上邊售票的例子

public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("視窗一:");
        w2.setName("視窗二:");
        w3.setName("視窗三:");

        w1.start();
        w2.start();
        w3.start();

    }
}

//此時的方法也是有bug的---執行緒不同步
class Window extends Thread{
    //要使用static修飾(多個視窗共享一個變數)
    public static int ticket = 20;//總票數
	//用來充當同步監視器,靜態可以保證唯一
	private static Object obj = new Object();
    @Override
    public void run() {
        while (true){
	        synchronized(obj){
	         //錯誤的方式:this代表著t1,t2,t3三個物件
//           synchronized (this){
	           if (ticket > 0) {
	                System.out.println(getName() + ticket);
	                ticket--;
	            } else {
	                break;
	       		}
	       	}
      	}
   	}
}

此時這個售票系統就是安全的,不會出現錯票和重票等問題。
只需要使用synchronized將需要被同步的程式碼包裹起來即可。

注意:synchronized使用的同步監視器(也就是括號中的物件)必須是唯一的,但凡有一個執行緒使用的同步監視器與其他執行緒不一致,那這個執行緒就不是安全的。

  • 方式二:同步方法
    將synchronized 放在方法宣告中,表示整個方法都是同步方法。
    public synchronized void show (){
    ….
    }

說明:

  1. 將需要被同步的程式碼寫在一個被synchronized修飾的方法中,然後在run中呼叫此方法
  2. 在同步方法中仍然涉及到同步監視器
  3. 在介面中synchronized修飾的方法,預設鎖的物件是this
  4. 在類中,預設的是類,若鎖唯一,預設的就是 類名.class

例:

class Window3 implements Runnable {

	//因為只需要建立一個Window3的物件,所以可以不適用static修飾
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
			//在run方法中呼叫同步方法
            show();
        }
    }

//同步方法
    private synchronized void show(){//同步監視器:this
        //synchronized (this){

            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);

                ticket--;
            }
        //}
    }
}


public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }

}
  1. 同步方法仍然涉及到同步監視器,只是不需要我們顯式的宣告。
  2. 非靜態的同步方法,同步監視器是:this
    靜態的同步方法,同步監視器是:當前類本身

關於同步機制中的鎖(同步監視器)
在這裡插入圖片描述

  • 如何判斷程式碼是否存線上程安全?(重要!!!)

    1. 明確哪些程式碼是多執行緒執行的程式碼
    2. 明確多個執行緒是否有共享資料
    3. 明確多執行緒執行程式碼中是否有多條語句操作共享資料
  • 如何解決
    對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。
    所有操作共享資料的這些語句都要放在同步範圍中

  • 注意:
    範圍太小:沒鎖住所有有安全問題的程式碼
    範圍太大:沒發揮多執行緒的功能。

(三)釋放鎖的操作

  1. 當前執行緒的同步方法、同步程式碼塊執行結束。
  2. 當前執行緒在同步程式碼塊、同步方法中遇到break、return終止了該程式碼塊、該方法的繼續執行。
  3. 當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束。
  4. 當前執行緒在同步程式碼塊、同步方法中執行了執行緒物件的wait()方法,當前執行緒暫停,並釋放鎖。

(四)不會釋放鎖的操作

  1. 執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()、Thread.yield()方法暫停當前執行緒的執行
  2. 執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放鎖(同步監視器)。
  3. 應儘量避免使用suspend()和resume()來控制執行緒

(五)執行緒的死鎖問題

  • 死鎖
  1. 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
  2. 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續
  • 解決方法
  1. 專門的演算法、原則
  2. 儘量減少同步資源的定義
  3. 儘量避免巢狀同步

死鎖的演示:

class A {
	public synchronized void foo(B b) { //同步監視器:A類的物件:a
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 進入了A例項的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 企圖呼叫B例項的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步監視器:A類的物件:a
		System.out.println("進入了A類的last方法內部");
	}
}

class B {
	public synchronized void bar(A a) {//同步監視器:b
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 進入了B例項的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 企圖呼叫A例項的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步監視器:b
		System.out.println("進入了B類的last方法內部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主執行緒");
		// 呼叫a物件的foo方法
		a.foo(b);
		System.out.println("進入了主執行緒之後");
	}

	public void run() {
		Thread.currentThread().setName("副執行緒");
		// 呼叫b物件的bar方法
		b.bar(a);
		System.out.println("進入了副執行緒之後");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();


		dl.init();
	}
}

(六)Lock鎖

  • 從JDK 5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當。
  • **java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。**鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件。
  • ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。

例:

class Window implements Runnable{

    private int ticket = 100;
    
    //1.例項化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.呼叫鎖定方法lock()----每次只能有一個執行緒進行上鎖,知道該執行緒釋放所以後其他執行緒才能進入,且只能進入一個
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票號為:" + ticket);
                    ticket--;
                }else{
                    break;
                }
                //一定要將unlock()方法寫在finally中,這樣是為了避免上邊的程式碼出現異常,導致永遠不會釋放鎖
            }finally {
            
                //3.呼叫解鎖方法:unlock()
                //如果不執行此操作,其他執行緒將永遠不會進入執行,導致多執行緒的程式變成了單執行緒
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}

(七)synchronized 與 Lock的異同?

相同:二者都可以解決執行緒安全問題

不同:

  1. synchronized機制在執行完相應的同步程式碼以後,自動的釋放同步監視器
  2. Lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())

注意: 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)

推薦優先使用順序:
Lock —> 同步程式碼塊(已經進入了方法體,分配了相應資源) —> 同步方法(在方法體之外)

建立執行緒的方式有四種
解決執行緒安全的方式有三種:同步程式碼塊、同步方法、lock鎖(JDK5.0 新增)

六:執行緒通訊

推薦:https://blog.csdn.net/u011635492/article/details/83043212

執行緒通訊,即執行緒之間進行的通訊,當一個執行緒滿足某些條件時,通知其他執行緒可以做哪些操作了。比如:經典的生產者消費者問題:當佇列滿時,生產者需要等待佇列有空間才能繼續往裡面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即佇列)的佔用權。日常生活中有很多類似的事情發生,在程式中也是必不可免的。為了解決這種問題,就出現了執行緒通訊的使用。

  • 涉及到的三個方法:
  1. wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器。
  2. notify():一旦執行此方法,就會喚醒被wait的一個執行緒。如果有多個執行緒被wait,就喚醒優先順序高的那個。
  3. notifyAll():一旦執行此方法,就會喚醒所有被wait的執行緒。
  • 說明:
  1. wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。
  2. wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。否則,會出現IllegalMonitorStateException異常
  3. wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中。
  • sleep() 和 wait()的異同?
  1. 相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。
  2. 不同點:
    1)兩個方法宣告的位置不同:Thread類中宣告sleep() , Object類中宣告wait()
    2)呼叫的要求不同:sleep()可以在任何需要的場景下呼叫。 wait()必須使用在同步程式碼塊或同步方法中
    3)關於是否釋放同步監視器:如果兩個方法都使用在同步程式碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

例:

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {

        while(true){

            synchronized (obj) {

                obj.notify();

                if(number <= 100){

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得呼叫如下wait()方法的執行緒進入阻塞狀態
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }

    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("執行緒1");
        t2.setName("執行緒2");

        t1.start();
        t2.start();
    }
}

生產者消費者問題:

//店員  用來和生產者消費者(可以想象為廚師顧客)的通訊
class Clerk{
    private int productCount = 0;
    //生產產品
    public synchronized void produceProduct() {

        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":開始生產第" + productCount + "個產品");

            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    //消費產品
    public synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":開始消費第" + productCount + "個產品");
            productCount--;

            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
//生產者
class Producer extends Thread{

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":開始生產產品.....");

        while(true){

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produceProduct();
        }

    }
}
//消費者
class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(getName() + ":開始消費產品.....");

        while(true){

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

public class ProductTest {

    public static void main(String[] args) {
    	//建立店員物件,
        Clerk clerk = new Clerk();
        //
        Producer p1 = new Producer(clerk);
        p1.setName("生產者1");
		
		//建立多個消費者物件
        Consumer c1 = new Consumer(clerk);
        c1.setName("消費者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消費者2");

        p1.start();
        c1.start();
        c2.start();

    }
}

七、JDK 5.0 新增建立執行緒方式

(一)新增方式一:實現Callable介面

  • 與使用Runnable相比, Callable功能更強大些
  1. 相比run()方法,可以有返回值
  2. 方法可以丟擲異常
  3. 支援泛型的返回值
  4. 需要藉助FutureTask類,比如獲取返回結果
  • Future介面
  1. 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
  2. FutrueTask是Futrue介面的唯一的實現類
  3. FutureTask 同時實現了Runnable, Future介面。它既可以作為Runnable被執行緒執行,又可以作為Future得到Callable的返回值
  • 如何理解實現Callable介面的方式建立多執行緒比實現Runnable介面建立多執行緒方式強大?
  1. call()可以有返回值的。
  2. call()可以丟擲異常,被外面的操作捕獲,獲取異常的資訊
  3. Callable是支援泛型的

例:

// 1.建立Callable介面的實現類,泛型可有可無
class NumThread implements Callable{
    // 2. 實現call方法,並將需要執行的程式碼寫在call()中
    @Override
    public Object call() throws Exception {//可以丟擲異常
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        // 返回的sum 是 int型,而方法的返回值為Object,此處有一個自動裝箱
        //在不需要返回值時,就return null;
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        // 3. 建立NumThread介面實現類的物件
        NumThread numThread = new NumThread();
        // 4. 將此NumThread介面實現類的物件作為引數傳遞到FutureTask構造器中,並建立FutureTask的物件
        FutureTask futureTask = new FutureTask(numThread);
        // 5. 將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件,並呼叫start()
        new Thread(futureTask).start();

        try {
            // 6. 獲取Callable中call的返回值
            // get()返回值即為futureTask構造器引數Callable中實現call()的返回值
            // 若沒有將實現介面類的物件傳遞到FutureTask中且在將FutureTask的物件傳遞到Thread類的物件中
            // 並呼叫start(), 則無法獲取Callable介面實現類中call()的返回值
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }
}

(二)新增方式二:使用執行緒池

  • 背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
  • 思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。
  • 好處:
  1. 提高響應速度(減少了建立新執行緒的時間)
  2. 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
  3. 便於執行緒管理
    ① corePoolSize:核心池的大小
    ② maximumPoolSize:最大執行緒數
    ③ keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

例:

public class ThreadPool {
    public static void main(String[] args) {
        // 1. 提供指定執行緒數量的執行緒池
        // ExecutorService是一個介面 Executors相當於工具類  service才是執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //因為service的實現類是ThreadPoolExecutor,即可以將service強轉過去
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //service本身就是ThreadPoolExecutor型別的

        //設定執行緒池的屬性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
        
        // 檢視service實現類 System.out.println(service.getClass());
        
        // 2. 執行指定執行緒的操作,需要提供Runnable介面實現類的物件或Callable介面實現類的物件
        service.execute(new NumberThread());//適用於Runnable
//        service.submit(Callable call);//適用於Callable

        // 3. 使用完之後關閉執行緒池
        service.shutdown();
    }
}

class NumberThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0)
                System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

注意:使用完一定要關閉執行緒池

相關文章