Java 多線程

1 多線程

1.1 多線程介紹

??學習多線程之前,我們先要了解幾個關于多線程有關的概念。
??進程:進程指正在運行的程序。確切的來說,當一個程序進入內存運行,即變成一個進程,進程是處于運行過程中的程序,并且具有一定獨立功能。

??線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之為多線程程序。
??簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程

??什么是多線程呢?即就是一個程序中有多個線程在同時執行。
??單線程程序:即,若有多個任務只能依次執行。當上一個任務執行結束后,下一個任務開始執行。如,去網吧上網,網吧只能讓一個人上網,當這個人下機后,下一個人才能上網。
??多線程程序:即,若有多個任務可以同時執行。如,去網吧上網,網吧能夠讓多個人同時上網。

1.2 程序運行原理

分時調度
??所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。
搶占式調度
??優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。

1.2.1 搶占式調度詳解

??大部分操作系統都支持多進程并發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開著畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行著“。

??實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行著高速的切換。對于CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
??其實,多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

1.3 主線程

??回想我們以前學習中寫過的代碼,當我們在dos命令行中輸入java空格類名回車后,啟動JVM,并且加載對應的class文件。虛擬機并會從main方法開始執行我們的程序代碼,一直把main方法的代碼執行結束。如果在執行過程遇到循環時間比較長的代碼,那么在循環之后的其他代碼是不會被馬上執行的。
??代碼演示如下:

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        System.out.println("start");
        show();
        System.out.println("end");
    }

    public static void show(){
        int sum = 0;
        for(int i = 0; i <= 100; i++){
            sum = sum + i;
            System.out.println("sum = "+ sum);
        }
    }
}

??若在上述代碼中show方法中的循環執行次數很多,這時在show();下面的代碼是不會馬上執行的,并且在dos窗口會看到不停的輸出sum=值,這樣的語句。為什么會這樣呢?
??原因是:jvm啟動后,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之為主線程。當程序的主線程執行時,如果遇到了循環而導致程序在指定位置停留時間過長,則無法馬上執行下面的程序,需要等待循環結束后能夠執行。
??那么,能否實現一個主線程負責執行其中一個循環,再由另一個線程負責其他代碼的執行,最終實現多部分代碼同時執行的效果?
??能夠實現同時執行,通過Java中的多線程技術來解決該問題。

1.4 Thread類

??該如何創建線程呢?通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程序中的執行線程。Java 虛擬機允許應用程序并發地運行多個執行線程。

??構造方法

??常用方法

??繼續閱讀,發現創建新執行線程有兩種方法。
??一種方法是將類聲明為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。創建對象,開啟線程。run方法相當于其他線程的main方法。
??另一種方法是聲明一個實現 Runnable 接口的類。該類然后實現 run 方法。然后創建Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。

1.5 創建線程方式一繼承Thread類

創建線程的步驟:
??1. 定義一個類繼承Thread。
??2. 重寫run方法。
??3. 創建子類對象,就是創建線程對象。
??4. 調用start方法,開啟線程并讓線程執行,同時還會告訴jvm去調用run方法。
自定義線程類MyThread .java

package com.qtw.api;

public class MyThread extends Thread {
    //定義指定線程名稱的構造方法
    public MyThread(String name) {
        //調用父類的String參數的構造方法,指定線程的名稱
        super(name);
    }
    /**
     * 重寫run方法,完成該線程執行的邏輯
     */
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":正在執行!"+i);
        }
    }
}

測試類TestDemo.java

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        //創建自定義線程對象
        MyThread mt1 = new MyThread("新的線程1");
        MyThread mt2 = new MyThread("新的線程2");
        //開啟新線程
        mt1.start();
        mt2.start();
        //在主方法中執行for循環
        for (int i = 0; i < 100; i++) {
            System.out.println("main線程!"+i);
        }
    }
}

??思考:線程對象調用 run方法和調用start方法區別?
??線程對象調用run方法不開啟線程。僅是對象調用方法。線程對象調用start開啟線程,并讓jvm調用run方法在開啟的線程中執行。

1.5.1 繼承Thread類原理

??我們為什么要繼承Thread類,并調用其的start方法才能開啟線程呢?
??繼承Thread類:因為Thread類用來描述線程,具備線程應該有功能。那為什么不直接創建Thread類的對象呢?如下代碼:

Thread t1 = new Thread();
t1.start();
//這樣做沒有錯,但是該start調用的是Thread類中的run方法,
//而這個run方法沒有做什么事情,更重要的是這個run方法中并沒有定義我們需要讓線程執行的代碼。

??創建線程的目的是什么?
??是為了建立程序單獨的執行路徑,讓多部分代碼實現同時執行。也就是說線程創建并執行需要給定線程要執行的任務。
??對于之前所講的主線程,它的任務定義在main函數中。自定義線程需要執行的任務都定義在run方法中。
??Thread類run方法中的任務并不是我們所需要的,只有重寫這個run方法。既然Thread類已經定義了線程任務的編寫位置(run方法),那么只要在編寫位置(run方法)中定義任務代碼即可。所以進行了重寫run方法動作。

1.5.2 多線程的內存圖解

??多線程執行時,到底在內存中是如何運行的呢?以上個程序為例,進行圖解說明,多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。

??當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那么進程就結束了。

1.5.3 獲取線程名稱

??開啟的線程都會有自己的獨立運行棧內存,那么這些運行的線程的名字是什么呢?該如何獲取呢?既然是線程的名字,按照面向對象的特點,是哪個對象的屬性和誰的功能,那么我們就去找那個對象就可以了。查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,我們就可以試試。

??Thread.currentThread()獲取當前線程對象
??Thread.currentThread().getName();獲取當前線程對象的名稱
自定義線程類MyThread.java

package com.qtw.api;

public class MyThread extends Thread {
    /**
     * 重寫run方法,完成該線程執行的邏輯
     */
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+",i="+i);
        }
    }
}

測試類TestDemo.java

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        //創建兩個線程任務
        MyThread d1 = new MyThread();
        MyThread d2 = new MyThread();
        d1.start();//開啟一個新線程,新線程調用run方法
        d2.start();//開啟一個新線程,新線程調用run方法
    }
}

??通過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1......
??進行多線程編程時,不要忘記了Java程序運行是從主線程開始,main方法就是主線程的線程執行內容。

1.6 創建線程方式—實現Runnable接口

??創建線程的另一種方法是聲明實現 Runnable 接口的類。該類然后實現 run 方法。然后創建Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。
??為何要實現Runnable接口,Runable是啥玩意呢?繼續API搜索。
??查看Runnable接口說明文檔:Runnable接口用來指定每個線程要執行的任務。包含了一個 run 的無參數抽象方法,需要由接口實現類重寫該方法。

接口中的方法

Thread類構造方法

創建線程的步驟。
??1. 定義類實現Runnable接口。
??2. 覆蓋接口中的run方法。。
??3. 創建Thread類的對象
??4. 將Runnable接口的子類對象作為參數傳遞給Thread類的構造函數。
??5. 調用Thread類的start方法開啟線程。
自定義線程執行任務類MyRunnable.java

package com.qtw.api;

public class MyRunnable implements Runnable{
    //實現run方法,同時定義線程要執行的run方法邏輯
    public void run(){
        for(int i = 1; i <= 100; i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

測試類TestDemo.java

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        //創建線程執行目標類對象
        Runnable runn1 = new MyRunnable();
        Runnable runn2 = new MyRunnable();
        //將Runnable接口的子類對象作為參數傳遞給Thread類的構造函數
        Thread thread1 = new Thread(runn1);
        Thread thread2 = new Thread(runn2);
        //開啟線程
        thread1.start();
        thread2.start();

    }
}

1.6.1 實現Runnable的原理

??為什么需要定一個類去實現Runnable接口呢?繼承Thread類和實現Runnable接口有啥區別呢?
??實現Runnable接口,避免了繼承Thread類的單繼承局限性。覆蓋Runnable接口中的run方法,將線程任務代碼定義到run方法中。
??創建Thread類的對象,只有創建Thread類的對象才可以創建線程。線程任務已被封裝到Runnable接口的run方法中,而這個run方法所屬于Runnable接口的子類對象,所以將這個子類對象作為參數傳遞給Thread的構造函數,這樣,線程對象創建時就可以明確要運行的線程的任務。

1.6.2 實現Runnable的好處

??第二種方式實現Runnable接口避免了單繼承的局限性,所以較為常用。實現Runnable接口的方式,更加的符合面向對象,線程分為兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一起。一旦創建Thread類的子類對象,既是線程對象,有又有線程任務。實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。

1.7 線程的匿名內部類使用

??使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。
??1. 創建線程對象時,直接重寫Thread類中的run方法

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        new Thread() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...X...." + x);
                }
            }
        }.start();
    }
}

??2. 使用匿名內部類的方式實現Runnable接口,重新Runnable接口中的run方法

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...Y...." + x);
                }
            }
        };
        new Thread(r).start();
    }
}

2 線程池

??線程池,其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。

??我們詳細的解釋一下為什么要使用線程池?
??在java中,如果每個請求到達就創建一個新線程,開銷是相當大的。在實際使用中,創建和銷毀線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創建和銷毀線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個jvm里創建太多的線程,可能會使系統由于過度消耗內存或“切換過度”而導致系統資源不足。為了防止資源不足,需要采取一些辦法來限制任何給定時刻處理的請求數目,盡可能減少創建和銷毀線程的次數,特別是一些資源耗費比較大的線程的創建和銷毀,盡量利用已有對象來進行服務。
??線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務重復使用線程,線程創建的開銷就被分攤到了多個任務上了,而且由于在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使用應用程序響應更快。另外,通過適當的調整線程中的線程數目可以防止出現資源不足的情況。

2.2 使用線程池方式--Runnable接口

??通常,線程池都是通過線程池工廠創建,再調用線程池中的方法獲取線程,再通過線程去執行任務方法。
??Executors:線程池創建工廠類

public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象

??ExecutorService:線程池類

Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,并執行

??Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用

使用線程池中線程對象的步驟:
??1. 創建線程池對象
??2. 創建Runnable接口子類對象
??3. 提交Runnable接口子類對象
??4. 關閉線程池
Runnable接口實現類

package com.qtw.api;

public class MyRunnable implements Runnable{
    //實現run方法,同時定義線程要執行的run方法邏輯
    public void run(){
        for(int i = 1; i <= 10; i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

代碼演示:

package com.qtw.api;

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class TestDemo{
    public static void main(String[] args) {
        //創建線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
        //創建Runnable實例對象
        MyRunnable r = new MyRunnable();

        //自己創建線程對象的方式
        //Thread t = new Thread(r);
        //t.start();// ---> 調用MyRunnable中的run()
        //從線程池中獲取線程對象,然后調用MyRunnable中的run()
        service.submit(r);
        //再獲取個線程對象,調用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        service.submit(r);
        //注意:submit方法調用結束后,程序并不終止,
        //是因為線程池控制了線程的關閉。將使用完的線程又歸還到了線程池中

        //關閉線程池,一般不進行次操作
        //service.shutdown();
    }
}

2.3 使用線程池方式—Callable接口

??Callable接口:與Runnable接口功能相似,用來指定線程的任務。其中的call()方法,用來返回線程任務執行完畢后的結果,call方法可拋出異常。
ExecutorService:線程池類

<T> Future<T> submit(Callable<T> task)
//獲取線程池中的某一個線程對象,并執行線程中的call()方法

Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用
使用線程池中線程對象的步驟:
??1. 創建線程池對象
??2. 創建Callable接口子類對象
??3. 提交Callable接口子類對象
??4. 關閉線程池
演示代碼

package com.qtw.api;

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class TestDemo{
    public static void main(String[] args) {
        //創建線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
        //創建Callable對象
        MyCallable c = new MyCallable();

        //從線程池中獲取線程對象,然后調用MyRunnable中的run()
        service.submit(c);

        //再獲取個教練
        service.submit(c);
        service.submit(c);
        //注意:submit方法調用結束后,程序并不終止,是因為線程池控制了線程的關閉。
        // 將使用完的線程又歸還到了線程池中
        //關閉線程池
        //service.shutdown();



    }
}

Callable接口實現類,call方法可拋出異常、返回線程任務執行完畢后的結果

package com.qtw.api;

import java.util.concurrent.Callable;

public class MyCallable implements Callable {
    //call的返回值類型是泛型,call()拋父類異常
    public Object call() throws Exception{
        for (int i = 1; i <= 10; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        return null;
    }
}

2.4 線程池練習:返回兩個數相加的結果

要求:通過線程池中的線程對象,使用Callable接口完成兩個數求和操作
Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用
V get() 獲取Future對象中封裝的數據結果
代碼演示:

package com.qtw.api;

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

public class TestDemo{
    public static void main(String[] args) throws Exception{
        //創建線程池對象
        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        //創建一個Callable接口子類對象
        //MyCallable c = new MyCallable();
        MyCallable c = new MyCallable(100, 200);
        MyCallable c2 = new MyCallable(10, 20);

        //獲取線程池中的線程,調用Callable接口子類對象中的call()方法, 完成求和操作
        //<Integer> Future<Integer> submit(Callable<Integer> task)
        // Future 結果對象
        Future<Integer> result = threadPool.submit(c);
        //此 Future 的 get 方法所返回的結果類型
        Integer sum = result.get();
        System.out.println("sum=" + sum);

        //再演示
        result = threadPool.submit(c2);
        sum = result.get();
        System.out.println("sum=" + sum);
        //關閉線程池(可以不關閉)



    }
}

Callable接口實現類

package com.qtw.api;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    //成員變量
    private int x = 5;
    private int y = 3;
    //構造方法
    public MyCallable(){
    }
    public MyCallable(int x, int y){
        this.x = x;
        this.y = y;
    }

    public Integer call() throws Exception {
        return x+y;
    }
}

3 多線程安全

3.1 線程安全

??如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
??我們通過一個案例,演示線程的安全問題:
??電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。
??我們來模擬電影院的售票窗口,實現多個窗口同時賣 “功夫熊貓3”這場電影票(多個窗口一起賣這100張票)需要窗口,采用線程對象來模擬;需要票,Runnable接口子類來模擬
測試類

package com.qtw.api;

public class TestDemo{
    public static void main(String[] args) {
        //創建票對象
        Ticket ticket = new Ticket();

        //創建3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");

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

模擬票

package com.qtw.api;

public class Ticket implements Runnable {
    //共100票
    private int ticket = 100;

    public void run() {
        //模擬賣票
        while (true) {
            if (ticket > 0) {
                //模擬選坐的操作
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }
}

運行結果發現:上面程序出現了問題
??票出現了重復的票
??錯誤的票 0、-1

??其實,線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

3.2 線程同步(線程安全處理Synchronized)

java中提供了線程同步機制,它能夠解決上述的線程安全問題。線程同步的方式有兩種:
??方式1:同步代碼塊
??方式2:同步方法

3.2.1 同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {
    可能會產生線程安全問題的代碼
}

??同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。
??使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:

package com.qtw.api;

public class Ticket implements Runnable {
    //共100票
    private int ticket = 100;
    Object lock = new Object();

    public void run() {
        //模擬賣票
        while (true) {
            synchronized (lock){
                if (ticket > 0) {
                    //模擬選坐的操作
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}

當使用了同步代碼塊后,上述的線程的安全問題,解決了。

3.2.2 同步方法

同步方法:在方法聲明上加上synchronized

public synchronized void method(){
    可能會產生線程安全問題的代碼
}

同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:

package com.qtw.api;

public class Ticket implements Runnable {
    //共100票
    private int ticket = 100;

    public void run() {
        //模擬賣票
        while (true) {
            if (ticket > 0) {
                //模擬選坐的操作
                method();
            }
        }

    }

    //同步方法,鎖對象this
    public synchronized void method(){
        if (ticket > 0) {
            //模擬選坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
}

靜態同步方法: 在方法聲明上加上static synchronized

public static synchronized void method(){
可能會產生線程安全問題的代碼
}

靜態同步方法中的鎖對象是 類名.class

3.3 死鎖

??同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程序出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

3.4 Lock接口

??查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。推薦使用
??Lock接口中的常用方法

??Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
??我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:

package com.qtw.api;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    //創建Lock鎖對象
    Lock ck = new ReentrantLock();

    public void run() {
        //模擬賣票
        while(true){
            //synchronized (lock){
            ck.lock();
            if (ticket > 0) {
                //模擬選坐的操作
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
            ck.unlock();
            //}
        }
    }
}

3.5 等待喚醒機制

??在開始講解等待喚醒機制之前,有必要搞清一個概念——線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
等待喚醒機制所涉及到的方法:
??wait():等待,將正在執行的線程釋放其執行資格 和 執行權,并存儲到線程池中。
??notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。
??notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。
??其實,所謂喚醒的意思就是讓 線程池中的線程具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程。
??仔細查看JavaAPI之后,發現這些方法 并不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,為什么這些操作線程的方法定義在Object類中?
??因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。

接下里,我們先從一個簡單的示例入手:

如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:
1.當input發現Resource中沒有數據時,開始輸入,輸入完成后,叫output來輸出。如果發現有數據,就wait();
2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然后,叫醒input來輸入數據。

下面代碼,模擬等待喚醒機制的實現:
模擬資源類

package com.qtw.api;

public class Resource {
    private String name;
    private String sex;
    private boolean flag = false;

    public synchronized void set(String name, String sex) {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 設置成員變量
        this.name = name;
        this.sex = sex;
        // 設置之后,Resource中有值,將標記該為 true ,
        flag = true;
        // 喚醒output
        this.notify();
    }

    public synchronized void out() {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 輸出線程將數據輸出
        System.out.println("姓名: " + name + ",性別: " + sex);
        // 改變標記,以便輸入線程輸入數據
        flag = false;
        // 喚醒input,進行數據輸入
        this.notify();
    }
}

輸入線程任務類

package com.qtw.api;

public class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int count = 0;
        while (true) {
            if (count == 0) {
                r.set("小明", "M");
            } else {
                r.set("小花", "F");
            }
            // 在兩個數據之間進行切換
            count = (count + 1) % 2;
        }
    }
}

輸出線程任務類

package com.qtw.api;

public class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.out();
        }
    }
}

測試類

package com.qtw.api;

public class TestDemo {
    public static void main(String[] args) {
        // 資源對象
        Resource r = new Resource();
        // 任務對象
        Input in = new Input(r);
        Output out = new Output(r);
        // 線程對象
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        // 開啟線程
        t1.start();
        t2.start();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。