快速瞭解Java多執行緒,有這一篇就夠了

偷得浮笙發表於2020-10-29

寫在前面,Java基礎系列文章都是作者基於b站尚矽谷的Java基礎視訊所做的筆記,沒有時間的同學可以認真看看,如果有時間的同學,還是建議看看視訊,畢竟筆記說到底還是自己的東西,每個人的習慣也是不一樣的,所以為了自己,學就對了,加油!
也請各位同學,如果感覺對自己有幫助,可以幫忙給個一鍵三連,謝謝!


1.基本概念:程式、程式、執行緒

  • 程式(program)是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼,靜態物件。
  • 程式(process)是程式的一次執行過程,或是正在執行的一個程式。是一個動態的過程:有它自身的產生、存在和消亡的過程。——生命週期
    • 如:執行中的QQ,執行中的MP3播放器程式是靜態的,程式是動態的
    • 程式作為資源分配的單位,系統在執行時會為每個程式分配不同的記憶體區域
  • 執行緒(thread),程式可進一步細化為執行緒,是一個程式內部的一條執行路徑。
    • 若一個程式同一時間並行執行多個執行緒,就是支援多執行緒的
    • 執行緒作為排程和執行的單位,每個執行緒擁有獨立的執行棧和程式計數器(pc),執行緒切換的開銷小
    • 一個程式中的多個執行緒共享相同的記憶體單元/記憶體地址空間—》它們從同一堆中分配物件,可以訪問相同的變數和物件。這就使得執行緒間通訊更簡便、高效。但多個執行緒操作共享的系統資源可能就會帶來安全的隱患。

image-20201029214356900

image-20201026203725591

  • 單核CPU和多核CPU的理解
    • 單核CPU,其實是一種假的多執行緒,因為在一個時間單元內,也只能執行一個執行緒的任務。例如:雖然有多車道,但是收費站只有一個工作人員在收費,只有收了費才能通過,那麼CPU就好比收費人員。如果有某個人不想交錢,那麼收費人員可以把他“掛起”(晾著他,等他想通了,準備好了錢,再去收費)。但是因為CPU時間單元特別短,因此感覺不出來。
    • 如果是多核的話,才能更好的發揮多執行緒的效率。(現在的伺服器都是多核的)
    • 一個Java應用程式java.exe,其實至少有三個執行緒:main( )主執行緒,gc( )垃圾回收執行緒,異常處理執行緒。當然如果發生異常,會影響主執行緒。
  • 並行與併發
    • 並行:多個CPU同時執行多個任務。比如:多個人同時做不同的事。
    • 併發:一個CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事。

1.1 使用多執行緒的優點

背景:以單核CPU為例,只使用單個執行緒先後完成多個任務(呼叫多個方法),肯定比用多個執行緒來完成用的時間更短,為何仍需多執行緒呢?

多執行緒程式的優點:

  1. 提高應用程式的響應。對圖形化介面更有意義,可增強使用者體驗。

  2. 提高計算機系統CPU的利用率

  3. 改善程式結構。將既長又複雜的程式分為多個執行緒,獨立執行,便於理解和修改

1.2 何時需要多執行緒

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

2.執行緒的建立和使用

2.1 執行緒的建立和啟動

  • Java語言的JVM允許程式執行多個執行緒,它通過java.lang.Thread類來體現。
  • Thread類的特性
    • 每個執行緒都是通過某個特定Thread物件的run( )方法來完成操作的,經常把run( )方法的主體稱為執行緒體
    • 通過該Thread物件的start( )方法來啟動這個執行緒,而非直接呼叫run( )

2.2 Thread類

構造器

  • Thread( ):建立新的Thread物件
  • Thread(String threadname):建立執行緒並指定執行緒例項名
  • Thread(Runnable target):指定建立執行緒的目標物件,它實現了Runnable介面中的run方法
  • Thread(Runnable target, String name):建立新的Thread物件

2.3 API中建立執行緒的兩種方式

JDK1.5之前建立新執行執行緒有兩種方法:

  • 繼承Thread類的方式
  • 實現Runnable介面的方式

2.3.1 建立多執行緒的方式一:繼承Thread類

/**
 * 多執行緒的建立,方式一:繼承於Thread類
 * 1.建立一個繼承於Thread類的子類
 * 2.重寫Thread類的run() --> 將此執行緒執行的操作宣告在run()中
 * 3.建立Thread類的子類的物件
 * 4.通過此物件呼叫start()
 *
 * 例子:遍歷100以內的所有的偶數
 */

//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(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        //3.建立Thread類的子類的物件
        MyThread t1 = new MyThread();

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

        //如下操作仍然是在main執行緒中執行的
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "*********main()***********");
            }
        }
    }

}

子執行緒的建立和啟動過程

image-20201026212532239

image-20201026212607290

2.3.2 建立過程中的兩個問題說明

/**
 * 多執行緒的建立,方式一:繼承於Thread類
 * 1.建立一個繼承於Thread類的子類
 * 2.重寫Thread類的run() --> 將此執行緒執行的操作宣告在run()中
 * 3.建立Thread類的子類的物件
 * 4.通過此物件呼叫start()
 *
 * 例子:遍歷100以內的所有的偶數
 */

//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(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        //3.建立Thread類的子類物件
        MyThread t1 = new MyThread();

        //4.通過此物件呼叫start(); 兩個作用:①啟動當前執行緒 ②呼叫當前執行緒的run()
        t1.start();
        //問題一:我們不能直接通過呼叫run()的方式啟動執行緒
//        t1.run();

        //問題二:再啟動一個執行緒,遍歷100以內的偶數。
        // 不可以還讓已經start()的執行緒去執行。會報IllegalThreadStateException異常
//        t1.start();

        //我們需要重新建立一個執行緒的物件
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main執行緒中執行的
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "************main()**************");
            }
        }
    }

}

2.4 Thread類練習

寫法一

/**
 * 練習:建立兩個分執行緒,其中一個執行緒遍歷100以內的偶數,另一個執行緒遍歷100以內的奇數
 */
public class ThreadDemo {

    public static void main(String[] args) {
        MyThread1 m1 = new MyThread1();
        MyThread2 m2 = new MyThread2();

        m1.start();
        m2.start();
    }
}

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

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

寫法二

/**
 * 練習:建立兩個分執行緒,其中一個執行緒遍歷100以內的偶數,另一個執行緒遍歷100以內的奇數
 */
public class ThreadDemo {

    public static void main(String[] args) {

        //建立Thread類的匿名子類的方式
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i % 2 == 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);

                    }
                }
            }
        }.start();

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

                    }
                }
            }
        }.start();

    }
}

2.5 Thread類的有關方法

/**
 * 測試Thread中的常用方法:
 * 1. start(): ①啟動當前執行緒;②呼叫當前執行緒的run()
 * 2. run(): 通常需要重寫Thread類中的此方法,將建立的執行緒要執行的操作宣告在此方法中
 * 3. currentThread():靜態方法,返回執行當前程式碼的執行緒
 * 4. getName():獲取當前執行緒的名字
 * 5. setName():設定當前執行緒的名字
 * 6. yield():釋放當前cpu的執行權,也就是執行緒禮讓,但是不一定成功
 * 7. join():線上程a中呼叫執行緒b的join(),此時執行緒a就進入阻塞狀態,
 *           直到執行緒b完全執行完以後,執行緒a才結束阻塞狀態。
 * 8. stop():已過時。當執行此方法時,強制結束當前執行緒。
 * 9. sleep(long millitime):讓當前執行緒"睡眠"指定的millitime毫秒。
 *                         在指定的millitime毫秒時間內,當前執行緒是阻塞狀態。
 * 10. isAlive():判斷當前執行緒是否存活
 */
class HelloThread extends Thread{
    @Override
    public void run() {
        for(int i = 0;i < 100; i++){

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

            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
//            if(i % 20 == 0){
//                yield();
//            }
        }
    }

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

public class ThreadModeTest {
    public static void main(String[] args) {
        HelloThread h1 = new HelloThread("Thread : 1");

//        h1.setName("執行緒一");

        h1.start();

        //給主執行緒命名
        Thread.currentThread().setName("主執行緒");

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

            if(i == 20){
                try {
                    h1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println(h1.isAlive());
    }
}

2.6 執行緒的排程

  • 排程策略
    • 時間片
      • image-20201026213517616
    • 搶佔式:高優先順序的執行緒搶佔CPU
  • Java的排程方法
    • 同優先順序執行緒組成先進先出佇列(先到先服務),使用時間片策略
    • 高優先順序,使用優先排程的搶佔式策略

2.7 執行緒的優先順序

/**
 * 執行緒的優先順序:
 * 1.
 *  MAX_PRIORITY:10
 *  MIN _PRIORITY:1
 *  NORM_PRIORITY:5  -->預設優先順序
 * 2.如何獲取和設定當前執行緒的優先順序:
 *   getPriority():獲取當前執行緒的優先順序
 *   setPriority(int p):設定當前執行緒的優先順序
 *
 *   說明:高優先順序的執行緒要搶佔低優先順序執行緒cpu的執行權。但這只是從概率上講,高優先順序的執行緒高概率的情況下被執行。並不意味著只有當高優先順序的執行緒執行完以後,低優先順序的執行緒才執行。
 */
class HelloThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){

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

                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }

//            if(i % 20 == 0){
//                yield();
//            }
        }
    }

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


public class ThreadMethodTest {
    public static void main(String[] args) {

        HelloThread h1 = new HelloThread("Thread1");

//        h1.setName("執行緒一");
        //設定分執行緒的優先順序
        h1.setPriority(Thread.MAX_PRIORITY);

        h1.start();

        //給主執行緒命名
        Thread.currentThread().setName("主執行緒");
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

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

//            if(i == 20){
//                try {
//                    h1.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }

        }

//        System.out.println(h1.isAlive());

    }
}

2.8 多執行緒練習

多視窗賣票

練習1

/**
 * 例子:建立三個視窗賣票,總票數為100張.(使用繼承Thread類的方式)
 *
 * 存線上程的安全問題,待解決。
 *
 * @author xiexu
 * @create 2020-09-12 下午 23:00
 */
class Window extends Thread{

    private static int ticket = 100;
    @Override
    public void run() {

        while(true){

            if(ticket > 0){
                System.out.println(getName() + ":賣票,票號為:" + ticket);
                ticket--;
            }else{
                break;
            }

        }

    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

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

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

練習2:

先看下面的建立多執行緒的方式二,再來看練習二

/**
 * 例子:建立三個視窗賣票,總票數為100張.(使用實現Runnable介面的方式)
 * 存線上程的安全問題,待解決。
 *
 * @author xiexu
 * @create 2020-09-12 下午 23:43
 */
class Window1 implements Runnable{

    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}


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

        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();
    }

}

2.9 建立多執行緒的方式二:實現Runnable介面

/**
 * 建立多執行緒的方式二:實現Runnable介面
 * 1. 建立一個實現了Runnable介面的類
 * 2. 實現類去實現Runnable介面中的抽象方法:run()
 * 3. 建立實現類的物件
 * 4. 將此物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件
 * 5. 通過Thread類的物件呼叫start()
 *
 * 比較建立執行緒的兩種方式。
 * 開發中:優先選擇:實現Runnable介面的方式
 * 原因:1. 實現的方式沒有類的單繼承性的侷限性
 *      2. 實現的方式更適合來處理多個執行緒有共享資料的情況。
 *
 * 聯絡:public class Thread implements Runnable
 * 相同點:兩種方式都需要重寫run(),將執行緒要執行的邏輯宣告在run()中。
 *        目前兩種方式,要想啟動執行緒,都是呼叫的Thread類中的start()
 *
 * @author xiexu
 * @create 2020-09-12 下午 11:34
 */

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

    //2. 實現類去實現Runnable介面中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 建立實現類的物件
        MThread mThread = new MThread();
        //4. 將此物件作為引數傳遞到Thread類的構造器中,建立Thread類的物件
        Thread t1 = new Thread(mThread);
        t1.setName("執行緒1");
        //5. 通過Thread類的物件呼叫start(): ①啟動執行緒 ②呼叫當前執行緒的run()-->呼叫了Runnable型別的target的run()
        t1.start();

        //再啟動一個執行緒,遍歷100以內的偶數
        Thread t2 = new Thread(mThread);
        t2.setName("執行緒2");
        t2.start();
    }

}

2.10 建立執行緒的兩種方式的聯絡與區別

/*
 * 比較建立執行緒的兩種方式。
 * 開發中:優先選擇:實現Runnable介面的方式
 * 原因:1. 實現的方式沒有類的單繼承性的侷限性
 *      2. 實現的方式更適合來處理多個執行緒有共享資料的情況。
 *
 * 聯絡:public class Thread implements Runnable
 * 相同點:兩種方式都需要重寫run(),將執行緒要執行的邏輯宣告在run()中。
 *        目前兩種方式,要想啟動執行緒,都是呼叫的Thread類中的start()
 */

2.11 補充:執行緒的分類

Java中的執行緒分為兩類:一種是守護執行緒,一種是使用者執行緒

  • 它們在幾乎每個方面都是相同的,唯一的區別是判斷JVM何時離開。

  • 守護執行緒是用來服務使用者執行緒的通過在start( )方法前呼叫thread.setDaemon(true)可以把一個使用者執行緒變成一個守護執行緒

  • Java垃圾回收就是一個典型的守護執行緒

  • 若JVM中都是守護執行緒,當前JVM將退出

  • 形象理解:兔死狗烹,鳥盡弓藏

3.執行緒的生命週期

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

  • 要想實現多執行緒,必須在主執行緒中建立新的執行緒物件。Java語言使用Thread類及其子類的物件來表示執行緒,在它的一個完整的生命週期中通常要經歷如下的五種狀態:

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

image-20201026220112044

image-20201026220233210

4.執行緒的同步

問題的提出:

多個執行緒執行的不確定性引起執行結果的不穩定性

多個執行緒對賬本的共享,會造成操作的不完整性,會破壞資料。

image-20201026220255374

4.1 例題:模擬火車站售票程式,開啟三個視窗售票。

//根據程式的執行結果可以看出當前程式存線上程安全問題
class Windows1 implements Runnable{

    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":賣票,票號為: " + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

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

        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();
    }
}

理想狀態

image-20201026220447141

極端狀態:1張票被取了三次

image-20201026220508317

4.2 同步程式碼塊處理實現Runnable的執行緒安全問題

/**
 *  例子:建立三個視窗賣票,總票數為100張.使用實現Runnable介面的方式
 *  1.賣票過程中出現重票、錯票 --> 出現了執行緒安全問題
 *  2.問題出現的原因:當某個執行緒操作車票的過程中,尚未操作完成時,其他執行緒參與進來,也在操作車票
 * 
 *  3.如何解決:當一個執行緒在操作ticket的時候,其他執行緒不能參與進來。直到執行緒a操作完ticket時,其他
 *            執行緒才可以操作ticket。這種情況即使執行緒a出現了阻塞,也不能被改變。
 * 
 *  4.在java中,我們通過同步機制,來解決執行緒的安全問題。
 *
 *  方式一:同步程式碼塊
 *  synchronized(同步監視器){
 *      //需要被同步的程式碼
 *  }
 *
 *  說明:1.操作共享資料的程式碼,即為需要被同步的程式碼 --->不能包含程式碼多了,也不能包含程式碼少了。
 *       2.共享資料:多個執行緒共同操作的變數。比如:ticket就是共享資料
 *       3.同步監視器,俗稱:鎖。任何一個類的物件,都可以拿來充當鎖。
 *          要求:多個執行緒必須要共用同一把鎖。
 *
 *       補充:在實現Runnable介面建立多執行緒的方式中,我們可以考慮使用this充當同步監視器。
 *
 *  方式二:同步方法
 *      如果操作共享資料的程式碼完整的宣告在一個方法中,我們不妨將此方法宣告同步的
 *
 *  5.同步的方式,解決了執行緒的安全問題。---好處
 *    操作同步程式碼時,只能有一個執行緒參與,其他執行緒等待。相當於是一個單執行緒的過程,效率低。---侷限性
 *
 * @author xiexu
 * @create 2020-09-13 上午 8:50
 */
class Window1 implements Runnable{

    private int ticket = 100;
//    Object obj = new Object();
//    Dog dog = new Dog();
    @Override
    public void run() {
//        Object obj = new Object(); 錯
        while(true){
            synchronized (this){//此時的this:唯一的Window1的物件   //方式二:synchronized (dog) {

                if (ticket > 0) {

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

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

                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

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

        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();
    }

}

class Dog{

}

image-20201026221331180

4.3 同步程式碼塊處理繼承Thread類的執行緒安全問題

/**
 * 使用同步程式碼塊來解決繼承Thread類的方式的執行緒安全問題
 *
 * 例子:建立三個視窗賣票,總票數為100張.使用繼承Thread類的方式
 *
 * 說明:在繼承Thread類建立多執行緒的方式中,
 *      慎用this充當同步監視器,考慮使用當前類充當同步監視器。
 *
 * @author xiexu
 * @create 2020-09-13 上午 10:31
 */
class Window2 extends Thread{

    private static int ticket = 100;

    private static Object obj = new Object();

    @Override
    public void run() {

        while(true){
            //正確的
//            synchronized (obj){
            synchronized (Window2.class){ //Class clazz = Window2.class; Window2.class只會載入一次
                //錯誤的方式:this代表著t1,t2,t3三個物件
//              synchronized (this){

                if(ticket > 0){

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

                    System.out.println(getName() + ":賣票,票號為:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }

        }

    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();

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

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

    }
}

4.4 同步方法處理實現Runnable的執行緒安全問題

/**
 * 使用同步方法解決 實現Runnable介面的執行緒安全問題
 *
 *  關於同步方法的總結:
 *  1. 同步方法仍然涉及到同步監視器,只是不需要我們顯式的宣告。
 *  2. 非靜態的同步方法,同步監視器是:this
 *     靜態的同步方法,同步監視器是:當前類本身
 *
 * @author xiexu
 * @create 2020-09-13 上午 11:19
 */

class Window3 implements Runnable {

    private int ticket = 100;

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

            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();
    }

}

4.5 同步方法處理繼承Thread類的執行緒安全問題

/**
 * 使用同步方法處理繼承Thread類的方式中的執行緒安全問題
 *
 * @author xiexu
 * @create 2020-09-13 上午 11:28
 */
class Window4 extends Thread {


    private static int ticket = 100;

    @Override
    public void run() {

        while (true) {

            show();
        }

    }
    private static synchronized void show(){//同步監視器:Window4.class
        //private synchronized void show(){ //同步監視器:t1,t2,t3。此種解決方式是錯誤的
        if (ticket > 0) {

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

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

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

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

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

    }
}

4.6 執行緒安全的單例模式之懶漢式

/**
 * 使用同步機制將單例模式中的懶漢式改寫為執行緒安全的
 *
 * @author xiexu
 * @create 2020-09-13 上午 11:38
 */
public class BankTest {

    public static void main(String[] args) {
        Bank bank1 = Bank.getInstance();
        Bank bank2 = Bank.getInstance();

        System.out.println(bank1 == bank2);
    }

}

class Bank{

    private Bank(){

    }

    private static Bank instance = null;

    public static Bank getInstance(){

        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }

        //方式二:效率更高
        if(instance == null){

            synchronized (Bank.class) {
                if(instance == null){

                    instance = new Bank();
                }
            }
        }
        return instance;
    }

}

4.7 死鎖問題

例1

/**
 * 演示執行緒的死鎖問題
 *
 * 1.死鎖的理解:不同的執行緒分別佔用對方需要的同步資源不放棄,
 * 都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
 *
 * 2.說明:
 * 1)出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續
 * 2)我們使用同步時,要避免出現死鎖。
 *
 * @author xiexu
 * @create 2020-09-13 下午 7:32
 */
public class ThreadTest {

    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {

                synchronized (s1){

                    s1.append("a");
                    s2.append("1");

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

                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){

                    s1.append("c");
                    s2.append("3");

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

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();

    }

}

例2

//死鎖的演示
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類的物件: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類的物件:b
		System.out.println("進入了B類的last方法內部");
	}
}

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

	//普通的init()方法
	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();
	}
}

4.8 Lock鎖方式解決執行緒安全問題

  • 從JDK 5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當。
  • java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件
  • ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖
/**
 * 解決執行緒安全問題的方式三:Lock鎖  --- JDK5.0新增
 *
 * 1. 面試題:synchronized 與 Lock的異同?
 *   相同:二者都可以解決執行緒安全問題
 *   不同:synchronized機制在執行完相應的同步程式碼以後,自動的釋放同步監視器
 *      Lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())
 *
 * 2.使用的優先順序:
 * Lock  <  同步程式碼塊(已經進入了方法體,分配了相應資源) <  同步方法(在方法體之外)
 *
 *  面試題:如何解決執行緒安全問題?有幾種方式
 * @author xiexu
 * @create 2020-09-14 上午 9:17
 */
class Window implements Runnable{

    private int ticket = 10000;

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

    @Override
    public void run() {
        while(true){
            try{
                //2.呼叫鎖定方法lock()
                lock.lock();

                if(ticket > 0){

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

                    System.out.println(Thread.currentThread().getName() + ":售票,票號為:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }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();
    }
}

4.9 練習

import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行有一個賬戶。
 * 有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。
 * 每次存完列印賬戶餘額。
 *
 * 分析:
 *    1.是否是多執行緒問題?是,兩個儲戶執行緒
 *    2.是否有共享資料?有,賬戶(或賬戶餘額)
 *    3.是否有執行緒安全問題?有
 *    4.需要考慮如何解決執行緒安全問題?同步機制:有三種方式。
 *
 * @author xiexu
 * @create 2020-09-14 上午 9:49
 */
class Account{
    private double balance; //餘額

    public Account(double balance) { //初始化餘額
        this.balance = balance;
    }

    //存錢
    public void deposit(double amt){
        if(amt > 0){
            balance += amt;

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

            System.out.println(Thread.currentThread().getName() + ":存錢成功。餘額為:" + balance);
        }
    }
}

class Customer extends  Thread{

    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        for (int i = 0; i < 3; i++) {

            lock.lock();

            acct.deposit(1000);

            lock.unlock();
        }

    }
}

public class AccountTest {

    public static void main(String[] args) {
        Account acct = new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

5.執行緒的通訊

/**
 * 執行緒通訊的例子:使用兩個執行緒列印 1-100。執行緒1, 執行緒2 交替列印
 *
 * 涉及到的三個方法:
 * wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器(同步鎖)。
 * notify():一旦執行此方法,就會喚醒被wait的一個執行緒。
 *           如果有多個執行緒被wait,就喚醒優先順序高的那個(其實也可以說是隨機喚醒)。
 * notifyAll():一旦執行此方法,就會喚醒所有被wait的執行緒。
 *
 * 說明:
 * 1.wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。
 * 2.wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。
 *    否則,會出現IllegalMonitorStateException異常
 * 3.wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中。
 *
 * 面試題:sleep() 和 wait()的異同?
 * 1.相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。
 * 2.不同點:1)兩個方法的位置不同:sleep()在Thread類裡面,
 *                              wait()是在Object裡面
 *          2)呼叫的要求不同:sleep()可以在任何需要的場景下呼叫。
 *                           wait()必須使用在同步程式碼塊或同步方法中(synchronized)
 *          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();
    }
}

5.1 面試題:sleep( ) 和 wait( )的異同?

/*
 * 面試題:sleep() 和 wait()的異同?
 * 1.相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。
 * 2.不同點:1)兩個方法的位置不同:sleep()在Thread類裡面,
 *                              wait()是在Object裡面
 *          2)呼叫的要求不同:sleep()可以在任何需要的場景下呼叫。
 *                           wait()必須使用在同步程式碼塊或同步方法中(synchronized)
 *          3)關於是否釋放同步監視器:
 *                               如果兩個方法都使用在同步程式碼塊或同步方法中,
 *                               sleep()不會釋放鎖,
 *                               wait()會釋放鎖。
 */

5.2 wait( ) 與 notify( ) 和 notifyAll( ) 三者的區別?

  • wait( ):令當前執行緒掛起並放棄CPU、同步資源並等待,使別的執行緒可訪問並修改共享資源,而當前執行緒排隊等候其他執行緒呼叫notify( )或notifyAll( )方法喚醒,喚醒後等待重新獲得對監視器的所有權後才能繼續執行。
  • notify( ):喚醒正在排隊等待同步資源的執行緒中優先順序最高者結束等待
  • notifyAll ( ):喚醒正在排隊等待資源的所有執行緒結束等待
  • 這三個方法只有在synchronized方法或synchronized程式碼塊中才能使用,否則會報 java.lang.IllegalMonitorStateException異常。
  • 因為這三個方法必須有鎖物件呼叫,而任意物件都可以作為synchronized的同步鎖, 因此這三個方法只能在Object類中宣告。

5.3 經典例題:生產者/消費者問題

在這裡插入圖片描述

/**
 * 執行緒通訊的應用:經典例題:生產者/消費者問題
 *
 * 生產者(Productor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,
 * 店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員
 * 會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品
 * 了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。
 *
 * 分析:
 * 1. 是否是多執行緒問題?是,生產者執行緒,消費者執行緒
 * 2. 是否有共享資料?是,店員(或產品)
 * 3. 如何解決執行緒的安全問題?同步機制,有三種方法
 * 4. 是否涉及到執行緒的通訊?是
 *
 * @author xiexu
 * @create 2020-09-14 上午 11:46
 */
//店員
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();
    }
}

6.JDK5.0新增執行緒建立方式

6.1 建立多執行緒的方式三:實現Callable介面

/**
 * 建立執行緒的方式三:實現Callable介面。 --- JDK 5.0新增
 *
 * 如何理解實現Callable介面的方式建立多執行緒比實現Runnable介面建立多執行緒方式強大?
 * 1. call()可以有返回值的。
 * 2. call()可以丟擲異常,被外面的操作捕獲,獲取異常的資訊
 * 3. Callable是支援泛型的
 *
 * @author xiexu
 * @create 2020-09-14 下午 6:38
 */
//1.建立一個實現Callable的實現類
class NumThread implements Callable{

    //2.實現call()方法,將此執行緒需要執行的操作宣告在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        //要求:遍歷100以內的數,返回偶數的和
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {

    public static void main(String[] args) {
        //3.建立Callable介面實現類的物件
        NumThread numThread = new NumThread();

        //4.將此Callable介面實現類的物件作為引數傳遞到FutureTask的構造器中,
        // 建立FutureTask的物件
        FutureTask futureTask = new FutureTask(numThread);

        //5.將FutureTask的物件作為引數傳遞到Thread類的構造器中,
        // 建立Thread物件,並呼叫start()
        //FutureTask實現了Runnable介面,
        // 所以Thread類構造器中的引數還是Runnable介面的實現類
        new Thread(futureTask).start();

        try {
            //6.獲取Callable中call()的返回值
            //get()返回值即為FutureTask構造器引數Callable介面的實現類重寫的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("總和為:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

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

6.2 使用執行緒池的好處

背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。

思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。

好處

  • 提高響應速度(減少了建立新執行緒的時間)
  • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
  • 便於執行緒管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大執行緒數
    • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

6.3 建立多執行緒的方式四:使用執行緒池

/**
 * 建立執行緒的方式四:使用執行緒池
 *
 * 好處:
 * 1.提高響應速度(減少了建立新執行緒的時間)
 * 2.降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
 * 3.便於執行緒管理
 *      corePoolSize:核心池的大小
 *      maximumPoolSize:最大執行緒數
 *      keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止
 *
 * 面試題:建立多執行緒有幾種方式?  四種!
 * @author xiexu
 * @create 2020-09-14 下午 9:01
 */

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);
            }
        }
    }
}

class NumberThread1 implements Runnable{

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

public class ThreadPool {

    public static void main(String[] args) {

        //1. 提供指定執行緒數量的執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //設定執行緒池的屬性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.執行指定的執行緒的操作。需要提供實現Runnable介面或Callable介面實現類的物件
        service.execute(new NumberThread());//適合適用於Runnable
        service.execute(new NumberThread1());//適合適用於Runnable

//        service.submit(Callable callable);//適合使用於Callable

        //3.關閉連線池
        service.shutdown();
    }

}

執行緒池相關API

  • JDK 5.0起提供了執行緒池相關API:ExecutorService 和 Executors
  • ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
    • void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行 Runnable
    • Future submit(Callable task):執行任務,有返回值,一般又來執行 Callable
    • void shutdown( ) :關閉連線池
  • Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池
    • Executors.newCachedThreadPool( ):建立一個可根據需要建立新執行緒的執行緒池
    • Executors.newFixedThreadPool(n); 建立一個可重用固定執行緒數的執行緒池
    • Executors.newSingleThreadExecutor( ) :建立一個只有一個執行緒的執行緒池
    • Executors.newScheduledThreadPool(n):建立一個執行緒池,它可安排在給定延遲後運 行命令或者定期地執行。

相關文章