Java多線程詳解——一篇文章搞懂Java多線程


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM