Java多執行緒詳解——一篇文章搞懂Java多執行緒

13roky發表於2021-04-27

1. 基本概念

  • 程式(program)

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

  • 程式(process)

    程式是程式的一次執行過程,也就是說程式執行起來了,載入到了記憶體中,並佔用了cpu的資源。這是一個動態的過程:有自身的產生、存在和消亡的過程,這也是程式的生命週期。

    程式是系統資源分配的單位,系統在執行時會為每個程式分配不同的記憶體區域。

  • 執行緒(thread)

    程式可進一步細化為執行緒,是一個程式內部的執行路徑。

    若一個程式同一時間並行執行多個執行緒,那麼這個程式就是支援多執行緒的。

    執行緒是cpu排程和執行的單位,每個執行緒擁有獨立的執行棧和程式計數器(pc),執行緒切換的開銷小。

    一個程式中的多個執行緒共享相同的記憶體單元/記憶體地址空間——》他們從同一堆中分配物件,可以訪問相同的變數和物件。這就使得相乘間通訊更簡便、搞笑。但索格執行緒操作共享的系統資源可能就會帶來安全隱患(隱患為到底哪個執行緒操作這個資料,可能一個執行緒正在操作這個資料,有一個執行緒也來操作了這個資料v)。

    • 配合JVM記憶體結構瞭解(只做瞭解即可)

      class檔案會通過類載入器載入到記憶體空間。

      其中記憶體區域中每個執行緒都會有虛擬機器棧和程式計數器。

      每個程式都會有一個方法區和堆,多個執行緒共享同一程式下的方法區和堆。

  • CPU單核和多核的理解

    單核的CPU是一種假的多執行緒,因為在一個時間單元內,也只能執行一個執行緒的任務。同時間段內有多個執行緒需要CPU去執行時,CPU也只能交替去執行多個執行緒中的一個執行緒,但是由於其執行速度特別快,因此感覺不出來。

    多核的CPU才能更好的發揮多執行緒的效率。

    對於Java應用程式java.exe來講,至少會存在三個執行緒:main()主執行緒,gc()垃圾回收執行緒,異常處理執行緒。如過發生異常時會影響主執行緒。

  • Java執行緒的分類:使用者執行緒 和 守護執行緒

    • Java的gc()垃圾回收執行緒就是一個守護執行緒
    • 守護執行緒是用來服務使用者執行緒的,通過在start()方法前呼叫thread.setDaemon(true)可以吧一個使用者執行緒變成一個守護執行緒。
  • 並行和併發

    • 並行:多個cpu同時執行多個任務。比如,多個人做不同的事。
    • 併發:一個cpu(採用時間片)同時執行多個任務。比如,渺少、多個人做同一件事。
  • 多執行緒的優點

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

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

2. 執行緒的建立和啟動

2.1. 多執行緒實現的原理

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

2.2.多執行緒的建立,方式一:繼承於Thread類

  1. 建立一個繼承於Thread類的子類。
  2. 重寫Thread類的run()方法。
  3. 建立Thread類的子類的物件。
  4. 通過此物件呼叫start()來啟動一個執行緒。

程式碼實現:多執行緒執行同一段程式碼

package com.broky.multiThread;

/**
 * @author 13roky
 * @date 2021-04-19 21:22
 */
public class ThreadTest extends Thread{
    @Override
    //執行緒體,啟動執行緒時會執行run()方法中的程式碼
    public void run() {
        //輸出100以內的偶數
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
        }
    }

    public static void main(String[] args) {
        //建立一個Thread類的子類物件
        ThreadTest t1 = new ThreadTest();
        //通過此物件呼叫start()啟動一個執行緒
        t1.start();
        //注意:已經啟動過一次的執行緒無法再次啟動
        //再建立一個執行緒
        ThreadTest t2 = new ThreadTest();
        t2.start();

        //另一種呼叫方法,此方法並沒有給物件命名
        new ThreadTest().start();

        System.out.println("主執行緒");
    }
}

多執行緒程式碼執行圖解

多執行緒執行多段程式碼

package com.broky.multiThread.exer;

/**
 * @author 13roky
 * @date 2021-04-19 22:43
 */
public class ThreadExerDemo01 {
    public static void main(String[] args) {
        new Thread01().start();
        new Thread02().start();
    }
}

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

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

2.3.多執行緒的建立,方式一:建立Thread匿名子類(也屬於方法一)

package com.broky.multiThread;

/**
 * @author 13roky
 * @date 2021-04-19 22:53
 */
public class AnonymousSubClass {
    public static void main(String[] args) {

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

    }
}

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

  1. 建立一個實現Runnable介面的類。
  2. 實現類去實現Runnable介面中的抽象方法:run()。
  3. 建立實現類的物件。
  4. 將此物件作為引數傳到Thread類的構造器中,建立Thread類的物件。
  5. 通過Thread類的物件呼叫start()方法。
package com.broky.multiThread;

/**
 * @author 13roky
 * @date 2021-04-20 23:16
 */
public class RunnableThread {
    public static void main(String[] args) {
        //建立實現類的物件
        RunnableThread01 runnableThread01 = new RunnableThread01();
        //建立Thread類的物件,並將實現類的物件當做引數傳入構造器
        Thread t1 = new Thread(runnableThread01);
        //使用Thread類的物件去呼叫Thread類的start()方法:①啟動了執行緒 ②Thread中的run()呼叫了Runnable中的run()
        t1.start();

        //在建立一個執行緒時,只需要new一個Thread類就可,不需要new實現類
        Thread t2 = new Thread(runnableThread01);
        t2.start();
    }
}

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

2.4.1. 比較建立執行緒的兩種方式

  • Java中只允許單程式,以賣票程式TiketSales類來說,很有可能這個類本來就有父類,這樣一來就不可以繼承Thread類來完成多執行緒了,但是一個類可以實現多個介面,因此實現的方式沒有類的單繼承性的侷限性,用實現Runnable介面的方式來完成多執行緒更加實用。
  • 實現Runnable介面的方式天然具有共享資料的特性(不用static變數)。因為繼承Thread的實現方式,需要建立多個子類的物件來進行多執行緒,如果子類中有變數A,而不使用static約束變數的話,每個子類的物件都會有自己獨立的變數A,只有static約束A後,子類的物件才共享變數A。而實現Runnable介面的方式,只需要建立一個實現類的物件,要將這個物件傳入Thread類並建立多個Thread類的物件來完成多執行緒,而這多個Thread類物件實際上就是呼叫一個實現類物件而已。實現的方式更適合來處理多個執行緒有共享資料的情況。
  • 聯絡:Thread類中也實現了Runnable介面
  • 相同點兩種方式都需要重寫run()方法,執行緒的執行邏輯都在run()方法中

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

與Runnable相比,Callable功能更強大

  1. 相比run()方法,可以有返回值
  2. 方法可以丟擲異常
  3. 支援泛型的返回值
  4. 需要藉助FutureTask類,比如獲取返回結果
package com.broky.multiThread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 建立執行緒的方式三:實現Callable介面。 ---JDK5新特性
 * 如何理解Callable比Runnable強大?
 * 1.call()可以有返回值
 * 2.call()可以丟擲異常被外面的操作捕獲
 * @author 13roky
 * @date 2021-04-22 21:04
 */

//1.建立一個實現Callable的實現類
class NumThread implements Callable<Integer>{
    //2.實現call方法,將此執行緒需要執行的操作宣告在call()中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        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<Integer> futureTask = new FutureTask(numThread);
        //5.將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread物件,並呼叫start()
        new Thread(futureTask).start();

        try {
            //6.獲取Callable中Call方法的返回值
            Integer sum = futureTask.get();
            System.out.println("總和為"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

2.6. 多執行緒的建立,方式四:執行緒池

背景:

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

思路:

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

優點:

​ 提高響應速度(減少了建立新執行緒的時間)

​ 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)

​ 便於執行緒管理

package com.broky.multiThread;

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

/**
 * 建立執行緒的方式四:使用執行緒池
 * <p>
 * 面試題:建立多執行緒有幾種方式
 *
 * @author 13roky
 * @date 2021-04-22 21:49
 */

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() + ":\t" + 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.submit(); 適合適用於Callable
        //關閉執行緒池
        service.shutdown();
    }
}

3. Thread類的常用方法

  • start() : 啟動當前執行緒, 呼叫當前執行緒的run()方法
  • run() : 通常需要重寫Thread類中的此方法, 將建立的執行緒要執行的操作宣告在此方法中
  • currentThread() : 靜態方法, 返回當前程式碼執行的執行緒
  • getName() : 獲取當前執行緒的名字
  • setName() : 設定當前執行緒的名字
  • yield() : 釋放當前CPU的執行權
  • join() : 線上程a中呼叫執行緒b的join(), 此時執行緒a進入阻塞狀態, 知道執行緒b完全執行完以後, 執行緒a才結束阻塞狀態
  • stop() : 已過時. 當執行此方法時,強制結束當前執行緒.
  • sleep(long militime) : 讓執行緒睡眠指定的毫秒數,在指定時間內,執行緒是阻塞狀態
  • isAlive() :判斷當前執行緒是否存活

4. 執行緒的排程

4.1. cpu的排程策略

  • 時間片:cpu正常情況下的排程策略。即CPU分配給各個程式的時間,每個執行緒被分配一個時間段,稱作它的時間片,即該程式允許執行的時間,使各個程式從表面上看是同時進行的。如果在時間片結束時程式還在執行,則CPU將被剝奪並分配給另一個程式。如果程式在時間片結束前阻塞或結束,則CPU當即進行切換。而不會造成CPU資源浪費。在巨集觀上:我們可以同時開啟多個應用程式,每個程式並行不悖,同時執行。但在微觀上:由於只有一個CPU,一次只能處理程式要求的一部分,如何處理公平,一種方法就是引入時間片,每個程式輪流執行。

  • 搶佔式:高優先順序的執行緒搶佔cpu。

4.2. Java的排程演算法:

  • 同優先順序執行緒組成先進先出佇列(先到先服務),使用時間片策略。
  • 堆高優先順序,使用優先排程的搶佔式策略。

執行緒的優先順序等級(一共有10擋)

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5 (預設優先順序)

獲取和設定當前執行緒的優先順序

  • getPriority(); 獲取
  • setPriority(int p); 設定

說明:高優先順序的執行緒要搶佔低優先順序執行緒cpu的執行權。但是隻是從概率上講,高優先順序的執行緒高概率的情況下被執行。並不意味著只有高優先順序的執行緒執行完成以後,低優先順序的執行緒才執行。

5. 執行緒的生命週期

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

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

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

6. 執行緒的同步

6.1. 多執行緒的安全性問題解析

  • 執行緒的安全問題
    • 多個執行緒執行的不確定性硬氣執行結果的不穩定性
    • 多個執行緒對賬本的共享, 會造成操作的不完整性, 會破壞資料.
    • 多個執行緒訪問共享的資料時可能存在安全性問題
  • 執行緒的安全問題Demo: 賣票過程中出現了重票和錯票的情況 (以下多視窗售票demo存在多執行緒安全問題)
package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-21 20:39
 */
public class SafeTicketsWindow {
    public static void main(String[] args) {
        WindowThread ticketsThread02 = new WindowThread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

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

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

class WindowThread implements Runnable {
    private int tiketsNum = 100;

    public void run() {
        while (true) {
            if (tiketsNum > 0) {
                try {
                    //手動讓執行緒進入阻塞,增大錯票概率
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum);
                /*try {
                    //手動讓執行緒進入阻塞,增大重票的概率
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                tiketsNum--;
            } else {
                break;
            }
        }
    }
}

錯票分析:

當票數為1的時候,三個執行緒中有執行緒被阻塞沒有執行票數-1的操作,這是其它執行緒就會通過if語句的判斷,這樣一來就會造成多賣了一張票,出現錯票的情況。

極端情況為,當票數為1時,三個執行緒同時判斷通過,進入阻塞,然後多執行兩側賣票操作。

重票分析

如果t1在輸出票號22和票數-1的操作之間被阻塞,這就導致這時候t1賣出了22號票,但是總票數沒有減少。在t1被阻塞期間,如果t2執行到輸出票號時,那麼t2也會輸出和t1相同的票號22.

通過以上兩種情況可以看出,執行緒的安全性問題時因為多個執行緒正在執行程式碼的過程中,並且尚未完成的時候,其他執行緒參與進來執行程式碼所導致的。

6.2. 多執行緒安全性問題的解決

原理:

當一個執行緒在操作共享資料的時候,其他執行緒不能參與進來。知道這個執行緒操作完共享資料的時候,其他執行緒才可以操作。即使當這個執行緒操作共享資料的時候發生了阻塞,依舊無法改變這種情況。

在Java中,我們通過同步機制,來解決執行緒的安全問題。

6.2.1. 多執行緒安全問題的解決方式一:同步程式碼塊

synchronized(同步監視器){需要被同步的程式碼}

說明:

  1. 操作共享資料(多個執行緒共同操作的變數)的程式碼,即為需要被同步的程式碼。 不能多包涵程式碼(效率低,如果包到while前面就變成了單執行緒了),也不能少包含程式碼
  2. 共享資料:多個執行緒共同操作的變數。
  3. 同步監視器:俗稱,鎖。任何一個類的物件都可以充當鎖。但是所有的執行緒都必須共用一把鎖,共用一個物件。

鎖的選擇:

  1. 自行建立,共用物件,如下面demo中的Object物件。

  2. 使用this表示當前類的物件

    繼承Thread的方法中的鎖不能使用this代替,因為繼承thread實現多執行緒時,會建立多個子類物件來代表多個執行緒,這個時候this指的時當前這個類的多個物件,不唯一,無法當作鎖。

    實現Runnable介面的方式中,this可以當作鎖,因為這種方式只需要建立一個實現類的物件,將實現類的物件傳遞給多個Thread類物件來當作多個執行緒,this就是這個一個實現類的物件,是唯一的,被所有執行緒所共用的物件。

  3. 使用類當作鎖,以下面demo為例,其中的鎖可以寫為WindowThread.class, 從這裡可以得出結論,類也是一個物件

優點:同步的方式,解決了執行緒安全的問題

缺點:操作同步程式碼時,只能有一個執行緒參與,其他執行緒等待。相當於時一個單執行緒的過程,效率低。

Demo

package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-21 20:39
 */
public class SafeTicketsWindow {
    public static void main(String[] args) {
        WindowThread ticketsThread02 = new WindowThread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

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

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

class WindowThread implements Runnable {
    private int tiketsNum = 100;
    
    //由於,Runnable實現多執行緒,所有執行緒共用一個實現類的物件,所以三個執行緒都共用實現類中的這個Object類的物件。
    Object obj = new Object();
    //如果時繼承Thread類實現多執行緒,那麼需要使用到static Object obj = new Object();
    
    public void run() {
        
        //Object obj = new Object();
        //如果Object物件在run()方法中建立,那麼每個執行緒執行都會生成自己的Object類的物件,並不是三個執行緒的共享物件,所以並沒有給加上鎖。
        
        while (true) {
            synchronized (obj) {
                if (tiketsNum > 0) {
                    try {
                        //手動讓執行緒進入阻塞,增大安全性發生的概率
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩餘票數:" + --tiketsNum);
                } else {
                    break;
                }
            }
        }
    }
}

6.3.2. 多執行緒安全問題的解決方式二:同步方法

將所要同步的程式碼放到一個方法中,將方法宣告為synchronized同步方法。之後可以在run()方法中呼叫同步方法。

要點:

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

Demo

package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-21 22:39
 */
public class Window02 {
    public static void main(String[] args) {
        Window02Thread ticketsThread02 = new Window02Thread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

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

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

class Window02Thread implements Runnable {
    private int tiketsNum = 100;

    @Override
    public void run() {
        while (tiketsNum > 0) {
            show();
        }
    }

    private synchronized void show() { //同步監視器:this
        if (tiketsNum > 0) {
            try {
                //手動讓執行緒進入阻塞,增大安全性發生的概率
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩餘票數:" + --tiketsNum);
        }
    }
}
package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-21 22:59
 */
public class Window03 {
    public static void main(String[] args) {
        Window03Thread t1 = new Window03Thread();
        Window03Thread t2 = new Window03Thread();
        Window03Thread t3 = new Window03Thread();
        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");
        t1.setPriority(Thread.MIN_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window03Thread extends Thread {
    public static int tiketsNum = 100;

    @Override
    public void run() {
        while (tiketsNum > 0) {
            show();
        }
    }

    public static synchronized void show() {//同步監視器:Winddoe03Thread.class  不加static話同步監視器為t1 t2 t3所以錯誤
        if (tiketsNum > 0) {
            try {
                //手動讓執行緒進入阻塞,增大安全性發生的概率
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩餘票數:" + --tiketsNum);
        }
    }
}

使用同步解決懶漢模式的執行緒安全問題

package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-22 7:24
 */
public class BankTest {
}

class Bank {
    private Bank() {
    }

    private static Bank instance = null;

    public static Bank getInstance() {
        //方式一:效率性差,每個等待執行緒都會進入同步程式碼塊
        //        synchronized (Bank.class) {
        //            if (instance == null) {
        //                instance = new Bank();
        //            }
        //        }

        //方式二:在同步程式碼塊外層在判斷一次,就防止所有執行緒進入同步程式碼塊。
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

6.2.3. 多執行緒安全問題的解決方式二:Lock鎖 -JDK5.0新特性

JDK5.0之後,可以通過例項化ReentrantLock物件,在所需要同步的語句前,呼叫ReentrantLock物件的lock()方法,實現同步鎖,在同步語句結束時,呼叫unlock()方法結束同步鎖

synchronized和lock的異同:(面試題)

1. Lcok是顯式鎖(需要手動開啟和關閉鎖),synchronized是隱式鎖,除了作用域自動釋放。
2. Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖。
3. 使用Lcok鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充性(提供更多的子類)

建議使用順序:Lock—》同步程式碼塊(已經進入了方法體,分配了相應的資源)—》同步方法(在方法體之外)

Demo:

package com.broky.multiThread.safeThread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 13roky
 * @date 2021-04-22 9:36
 */
public class SafeLock {
    public static void main(String[] args) {
        SafeLockThread safeLockThread = new SafeLockThread();
        Thread t1 = new Thread(safeLockThread);
        Thread t2 = new Thread(safeLockThread);
        Thread t3 = new Thread(safeLockThread);

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

class SafeLockThread implements Runnable{
    private int tickets = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (tickets>0) {
            try {
                //在這裡鎖住,有點類似同步監視器
                lock.lock();
                if (tickets > 0) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + ":\t票號:" + tickets + "\t剩餘票數:" + --tickets);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //操作完成共享資料後在這裡解鎖
                lock.unlock();
            }
        }
    }
}

6.3. 執行緒同步的死鎖問題

原理:

​ 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了死鎖。

​ 出現死鎖後,並不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續。

​ 使用同步時應避免出現死鎖。

Java中死鎖最簡單的情況:

​ 一個執行緒T1持有鎖L1並且申請獲得鎖L2,而另一個執行緒T2持有鎖L2並且申請獲得鎖L1,因為預設的鎖申請操作都是阻塞的,所以執行緒T1和T2永遠被阻塞了。導致了死鎖。這是最容易理解也是最簡單的死鎖的形式。但是實際環境中的死鎖往往比這個複雜的多。可能會有多個執行緒形成了一個死鎖的環路,比如:執行緒T1持有鎖L1並且申請獲得鎖L2,而執行緒T2持有鎖L2並且申請獲得鎖L3,而執行緒T3持有鎖L3並且申請獲得鎖L1,這樣導致了一個鎖依賴的環路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導致了死鎖。

​ 從這兩個例子,我們可以得出結論,產生死鎖可能性的最根本原因是:執行緒在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,並且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產生死鎖的最根本原因。另一個原因是預設的鎖申請操作是阻塞的

死鎖的解決方法:

1. 專門的演算法、原則。
2. 儘量減少同步資源的定義。
3. 儘量避免巢狀同步。
package com.broky.multiThread.safeThread;

/**
 * @author 13roky
 * @date 2021-04-22 8:34
 */
public class DeadLock {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread() {
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(1000);
                    } 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() {
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

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

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

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

7. 執行緒的通訊

很多情況下,儘管我們建立了多個執行緒,也會出現幾乎一個執行緒執行完所有操作的時候,這時候我們就需要讓執行緒間相互交流。

原理:

​ 當一個執行緒執行完成其所應該執行的程式碼後,手動讓這個執行緒進入阻塞狀態,這樣一來,接下來的操作只能由其他執行緒來操作。當其他執行緒執行的開始階段,再手動讓已經阻塞的執行緒停止阻塞,進入就緒狀態,雖說這時候阻塞的執行緒停止了阻塞,但是由於現在正在執行的執行緒拿著同步鎖,所以停止阻塞的執行緒也無法立馬執行。如此操作就可以完成執行緒間的通訊。

所用的到方法:

​ wait():一旦執行此方法,當前執行緒就會進入阻塞,一旦執行wait()會釋放同步監視器。

​ notify():一旦執行此方法,將會喚醒被wait的一個執行緒。如果有多個執行緒被wait,就喚醒優先度最高的。

​ notifyAll() :一旦執行此方法,就會喚醒所有被wait的執行緒

​ 說明:

​ 這三個方法必須在同步程式碼塊或同步方法中使用。

​ 三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。

​ 這三個方法並不時定義在Thread類中的,而是定義在Object類當中的。因為所有的物件都可以作為同步監視器,而這三個方法需要由同步監視器呼叫,所以任何一個類都要滿足,那麼只能寫在Object類中。

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

  1. 相同點:兩個方法一旦執行,都可以讓執行緒進入阻塞狀態。

  2. 不同點:1) 兩個方法宣告的位置不同:Thread類中宣告sleep(),Object類中宣告wait()

    ​ 2) 呼叫要求不同:sleep()可以在任何需要的場景下呼叫。wait()必須在同步程式碼塊中呼叫。

    ​ 2) 關於是否釋放同步監視器:如果兩個方法都使用在同步程式碼塊呵呵同步方法中,sleep不會釋放鎖,wait會釋放鎖。

Demo:

package com.broky.multiThread;

/**
 * @author 13roky
 * @date 2021-04-22 13:29
 */
public class Communication {
    public static void main(String[] args) {
        CommunicationThread communicationThread = new CommunicationThread();
        Thread t1 = new Thread(communicationThread);
        Thread t2 = new Thread(communicationThread);
        Thread t3 = new Thread(communicationThread);

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

class CommunicationThread implements Runnable {
    int Num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notifyAll();
                if (Num <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":\t" + Num);
                    Num++;

                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }

        }
    }
}

練習

  • 練習1:

銀行有一個賬戶。

有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完列印賬戶餘額。

package com.broky.multiThread.exer;

/**
 * 練習1
 * 銀行有一個賬戶
 * 有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完列印賬戶餘額。
 * 分析:
 * 1.是否有多個執行緒問題? 是,有兩個儲戶執行緒。
 * 2.是否有共享資料? 是,兩個儲戶向同一個賬戶存錢
 * 3.是否有執行緒安全問題: 有
 *
 * @author 13roky
 * @date 2021-04-22 12:38
 */
public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account();
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("儲戶1");
        c2.setName("儲戶2");

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

    }
}

class Account {
    private double accountSum;

    public Account() {
        this.accountSum = 0;
    }

    public Account(double accountSum) {
        this.accountSum = accountSum;
    }

    //存錢
    public void deppsit(double depositNum) {
        synchronized (this) {
            if (depositNum > 0) {
                accountSum = accountSum + depositNum;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ": 存錢成功,當前餘額為:\t" + accountSum);
            }
        }

    }

}

class Customer extends Thread {
    private Account acct;

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

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            acct.deppsit(1000);
        }
    }
}
  • 經典例題:生產者和消費著問題

生產者( Productor)將產品交給店員( Clerk),而消費者( (Customer)從店員處取走產品, 店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產; 如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。

package com.broky.multiThread.exer;

/**
 * - 經典例題:生產者和消費著問題
 * 生產者( Productor)將產品交給店員( Clerk),而消費者( (Customer)從店員處取走產品,
 * 店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員會叫生產者停一下,
 * 如果店中有空位放產品了再通知生產者繼續生產; 如果店中沒有產品了,店員會告訴消費者等一下,
 * 如果店中有產品了再通知消費者來取走產品。
 *
 * 分析:
 * 1.是多執行緒問題,可以假設多個消費這和多個生產者是多執行緒的
 * 2.存在操作的共享資料,生產和購買時都需要操作經銷商的庫存存量。
 * 3.處理執行緒安全問題。
 * 4.三個類:生產者,經銷商,消費者。經銷商被生產者和消費者共享。生產者讀取經銷商庫存,當庫存不夠時,生產產品
 * 併發給經銷商,操作經銷商庫存+1。消費者讀取經銷商庫存,當有庫存時,方可進行購買,購買完成後,經銷商庫存-1.
 * @author 13roky
 * @date 2021-04-22 14:36
 */
public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);
        Producer p2 = new Producer(clerk);
        p1.setName("生產者1");
        p2.setName("生產者2");

        Consumer c1 = new Consumer(clerk);
        Consumer c2 = new Consumer(clerk);
        c1.setName("消費者1");
        c2.setName("消費者2");

        p1.start();
        c1.start();
    }
}

class Clerk {
    private int productNum;

    public Clerk() {
        this.productNum = 0;
    }

    public int getProductNum() {
        return productNum;
    }

    public void setProductNum(int productNum) {
        this.productNum = productNum;
    }
}

class Producer extends Thread {
    private Clerk clerk;

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

        while(true){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            produce();
        }
    }

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

    private void produce() {
        synchronized (ProductTest.class) {
            ProductTest.class.notify();
            if (clerk.getProductNum() < 20) {
                clerk.setProductNum(clerk.getProductNum() + 1);
                System.out.println(Thread.currentThread().getName() + ":\t生產完成第 " + clerk.getProductNum() + " 個產品");
            }else {
                try {
                    ProductTest.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

class Consumer extends Thread {
    private Clerk clerk;

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始消費......");

        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();
        }
    }

    public Consumer(Clerk clerk) {
        if (clerk != null) {
            this.clerk = clerk;
        }
    }

    private void buy(){
        synchronized (ProductTest.class) {
            ProductTest.class.notify();
            if (clerk.getProductNum() > 0) {
                System.out.println(Thread.currentThread().getName() + ":\t購買完成第 " + clerk.getProductNum() + " 個產品");
                clerk.setProductNum(clerk.getProductNum() - 1);
            }else {

                try {
                    ProductTest.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

相關文章