1. 程式人生 > >Java 執行緒基礎

Java 執行緒基礎

文章目錄

1 Java執行緒基礎

案例中涉及程式碼 --> 點選跳轉

1.1 並行、序列與併發

場景:餐廳後廚
序列:大廚做完乾煸豆角後,又做了一盤椒鹽蘑菇;
並行:大廚同時開了兩個灶,同時做乾煸豆角和椒鹽蘑菇;
併發:大廚和他的3個徒弟同時開了8個灶,做了4份乾煸豆角和椒鹽蘑菇;

1.2 執行緒生命週期

每個執行緒都有自己的區域性變量表、程式計數器和生命週期:

  • NEW 新建
  • RUNNABLE 可執行
  • RUNNING 執行中
  • BLOCKED 阻塞
  • TERMINATED 結束

除了new和terminated,其他各個狀態之間會在不同的條件下會進行轉換:
這裡寫圖片描述

1.2.1 NEW

new一個Thread例項時,進入NEW狀態,這時的thread例項只是一個簡單的物件:

Thread thread = new Thread();

1.2.2 RUNNABLE

start()方法讓一個thread例項進入RUNNABEL狀態,這時才真正在JVM中建立了一個執行緒,等待CPU的排程獲得執行權:

Thread thread = new Thread();
thread.start();

1.2.3 RUNNING

一旦CPU通過輪詢或其他方式從執行佇列中選中該執行緒,此時才會真正執行執行緒中的邏輯程式碼,進入RUNNING狀態;

1.2.4 BLCOKED

如下方法可以讓一個執行緒進入BLCOKED狀態:

Object的wait方法
Thread的sleep方法
Thread的join方法
InterruptibleChannel的io操作
Selector的wakeup方法

1.2.5 TERMINATED

ERMINATED狀態是一個現成的最終狀態,處於該狀態後執行緒生命週期也就結束了,不會在切換至其它狀態了;

1.3 執行緒執行單元

1.3.1 執行單元

每個執行緒的業務邏輯部分被稱為執行單元,而邏輯部分都是寫在run()方法中的,也就是說執行緒的執行單元就是run()方法;

***建立執行緒***有一種方式:構造Thread類;
***實現執行單元***有兩種方式:
1)重寫Thread類的run()方法 ;

public class ThreadChild extends Thread {
    @Override
    public void run() {
        Thread.currentThread().setName("Thread's child");
        for (int i = 0; i < 10; i++)
            System.out.println(Thread.currentThread().getName() + "------------");
    }
}

2)實現Runnable介面的runnable方法;

public class RunnableImpl implements Runnable {
    public void run() {
        Thread.currentThread().setName("Runnable's implemention");
        for (int i = 0; i < 10; i++)
            System.out.println(Thread.currentThread().getName() + "------------");
    }
}

1.3.2 Thread中的run()方法–模板方法的應用

        //new一個Thread物件
        Thread thread = new Thread();

在直接new一個Thread物件的時候,run()方法中是一個空實現,看下Thread中的run()方法:
這裡寫圖片描述
看13.1中的第一個例子,執行邏輯run()方法是在子類ThreadChild中實現了邏輯細節,這是一個典型的模板方法;

1.4 Thread&Runnabel的關係

1.4.1 類圖上的關係

這裡寫圖片描述
Runnable現為一個函式式介面,Thread類是Runnable的一個實現類,實現了Runnable的run()方法,Thread不僅有run()方法,還有很多其他重要的方法來管理自己例項化執行緒的生命週期;

1.4.2 Runnable較Thread易於共享資源

案例為營業大廳取號機,從資源共享角度看通過實現Runnable介面更易於資源共享:
1)用Thread實現執行單元:

public class TicketWindow extends Thread {
    //櫃檯名稱
    private final String name;
    //最多受理的業務數
    private static final int max = 10;

    private static int index = 1;

    public TicketWindow(String name) {
        this.name = name;

    }

    @Override
    public void run() {
        while (index < max)
            System.out.println("櫃檯:" + name + "當前號碼為:" + (index++));
    }
}

呼叫方:

public static void main(String[] args) {
        TicketWindow ticketWindow1 = new TicketWindow("一號取號機");
        TicketWindow ticketWindow2 = new TicketWindow("二號取號機");
        TicketWindow ticketWindow3 = new TicketWindow("三號取號機");
        //三個TicketWindow執行緒公用靜態MAX[10]
        ticketWindow1.start();
        ticketWindow2.start();
        ticketWindow3.start();
    }

執行結果:

櫃檯:一號取號機當前號碼為:1
櫃檯:一號取號機當前號碼為:2
櫃檯:一號取號機當前號碼為:3
櫃檯:一號取號機當前號碼為:4
櫃檯:一號取號機當前號碼為:5
櫃檯:一號取號機當前號碼為:6
櫃檯:一號取號機當前號碼為:7
櫃檯:一號取號機當前號碼為:8
櫃檯:一號取號機當前號碼為:9

1)用Runnable實現執行單元:

public class TicketRunnable implements Runnable {

    private int index = 1;//不用static修改
    private final static int MAX = 10;

    public void run() {
        while (index <= MAX) {
            System.out.println(Thread.currentThread().getName() + "號碼是:" + (index++));

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

呼叫方:

public static void main(String[] args) {
        TicketRunnable ticketRunnable = new TicketRunnable();
        Thread ticketWindow4 = new Thread(ticketRunnable,"四號取號機");
        Thread ticketWindow5 = new Thread(ticketRunnable,"五號取號機");
        Thread ticketWindow6 = new Thread(ticketRunnable,"六號取號機");
        //三個執行緒公用一個runnable的私有屬性MAX[10]
        ticketWindow4.start();
        ticketWindow5.start();
        ticketWindow6.start();
    }

執行結果:

四號取號機號碼是:1
五號取號機號碼是:2
六號取號機號碼是:3
四號取號機號碼是:4
五號取號機號碼是:5
六號取號機號碼是:6
四號取號機號碼是:7
六號取號機號碼是:8
五號取號機號碼是:7
四號取號機號碼是:9
六號取號機號碼是:10

1.4.3 Thread&Runnabel的關係–策略模式的應用

1.4.3.1 策略模式

要了解Thread與Runnable的關係需要先了解策略模式:
策略模式:業務和行為解耦,職責分明,不同上下環境下執行不同的策略;
參見1–>菜鳥教程
參見2–>我的案例

1.4.3.2 Java執行緒策略模式的應用

上下文角色:Thread -->控制執行緒生命週期
策略角色:Runnable實現類 --> 不同的實現類是不同的策略,實現不同的邏輯
執行緒策略模式的應用 --> 不同的業務場景下生成不同業務邏輯的執行緒,業務邏輯和執行緒生命週期控制解構,Thread和Runnable職責分明:

public static void main(String[] args) {
        //策略角色:不同的Runnable實現類
        ThreeYearsOldRunnable threeYearsOldRunnable = new ThreeYearsOldRunnable();
        TenYearsOldRunnable tenYearsOldRunnable = new TenYearsOldRunnable();
        EighteenYearsOldRunnable eighteenYearsOldRunnable = new EighteenYearsOldRunnable();
        TwentyFiveYearsOldRunnable twentyFiveYearsOldRunnable = new TwentyFiveYearsOldRunnable();
        ThirtyYearsOldRunnable thirtyYearsOldRunnable = new ThirtyYearsOldRunnable();

        //上下文角色:Thread例項
        Thread threeYearsOld = new Thread(threeYearsOldRunnable, "三歲");
        Thread tenYearsOld = new Thread(tenYearsOldRunnable, "十歲");
        Thread eighteenYearsOld = new Thread(eighteenYearsOldRunnable, "十八歲");
        Thread wentyFiveYearsOld = new Thread(twentyFiveYearsOldRunnable, "二十五歲");
        Thread thirtyYearsOld = new Thread(thirtyYearsOldRunnable, "三十歲");

        //不同的上下文下
        for (int age = 0; age <= 100; age++) {
            if (3 == age)
                threeYearsOld.start();
            if (10 == age)
                tenYearsOld.start();
            if (18 == age)
                eighteenYearsOld.start();
            if (25 == age)
                wentyFiveYearsOld.start();
            if (30 == age)
                thirtyYearsOld.start();
        }
    }

執行結果:

三歲:叔叔好~我還有很多玩具要玩~
十歲:我在三年級學習,數學好難~
十八歲:告別難忘的高中時代,期待我的大學生活~
二十五歲:告別大學,初入職場處處碰壁~
三十歲: 我有了孩子,才知道我的爸媽多麼不容易~

1.4.4 Runnable&Thread關係總結

  • Thread是Runnable的實現類,實現了Runnable的run()方法,但是個空方法,應用了模板模式,具體的實現交給子類;
  • Thread和Runnable應用了策略模式,Thread管理執行緒的生命週期,Runnable實現類實現業務邏輯,兩者職責分明;
  • 建立執行緒時Runnable實現類較Thread子類能更好更容易地線上程間共享資源;

2 Thread建構函式

從Thread構造方法來看Thread的名字、與ThreadGroup和Jvm Stack的關係;

2.1 執行緒命名

在常見執行緒的時候可以給執行緒指定一個名字,便於在多執行緒程式中查詢問題;

2.1.1 執行緒的預設命名

  • Thread()
  • Thread(Runnable Target)
  • Thread(ThreadGroup group, Runnable Target)

這三個構造方法沒有提供執行緒命名的引數,執行緒會進行如下命名:以"Thread-"作為字首與一個自增數進行組合,自增數會隨著jvm程序中執行緒的數量不斷自增

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }        

啟了一個執行緒,被預設命名為"Thread-0":
這裡寫圖片描述
可以線上程啟動前為其命名:

new Thread(() ->
        {
            //為其命名
            Thread.currentThread().setName("weixx");
            try {
                TimeUnit.SECONDS.sleep(100l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

這裡寫圖片描述

2.1.2 用帶有命名引數的構造方法為執行緒命名

  • public Thread(String name)
  • public Thread(ThreadGroup group, String name)
  • public Thread(Runnable target, String name)
  • public Thread(ThreadGroup group, Runnable target, String name)
  • public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

2.2 ThreadGroup

  • public Thread(ThreadGroup group, String name)
  • public Thread(ThreadGroup group, Runnable target)
  • public Thread(ThreadGroup group, Runnable target, String name)
  • public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

在Thread構造方法中:

  • 可以顯式地為執行緒指定group
  • 如果沒有顯式地指定一個ThreadGroup,則會將其加入其父執行緒所在的執行緒組
    這裡寫圖片描述
    建立兩個執行緒,一個構造時候指定ThreadGroup,一個不指定:
    這裡寫圖片描述
    注:main方法的ThreadGroup為"main"

2.3 JVM Stack 虛擬機器棧

需要先了解JVM記憶體結構–>[importnew]

2.3.1 帶有stackSize引數的構造方法

  • public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

執行緒建立時,除了這個構造方法顯式的指定了stackSize,其它構造方法都使用了預設值"0":
這裡寫圖片描述
官方文件對stackSize的解釋:

  • stackSize越大代表著當前執行緒內方法呼叫遞迴深度越深
  • stackSize越小代表著建立執行緒的數量越多
    當程式進行無限制深度遞迴時,Java棧中會不斷地進行壓棧彈棧操作,JVM的記憶體大小是有限的,終有被壓爆的時候,最後會丟擲StackOverFlowError異常,stackSize數量級大小與遞迴深度成正比,該引數一般不會主動設定,採用系統預設值"0"就好;

2.3.2 Java棧深度

棧幀結構圖:
這裡寫圖片描述
每個執行緒建立的時候,JVM會為其建立Java棧,Java棧的大小可以通過-xss引數調整,方法呼叫就是棧幀被壓入和彈出的過程,由結構圖可以看出Java虛擬機器棧大小相同的情況下,區域性變量表等佔用的記憶體越小,可以被壓入的棧幀就越多,反之越少,棧幀的大小被稱為寬度,棧幀的數量被成為Java棧的深度;

2.3.3 Thread與Java棧

程序的大小 ≈ 堆記憶體 + 執行緒數量 * 棧記憶體
執行緒數量 = (最大地址空間(MaxProcessMemory) - JVM堆記憶體 - ReservedOsMemory)/ThreadStackSize(XSS)

結論:執行緒數量與Java棧記憶體大小成反比,與堆記憶體成反比

注:ReservedOsMemory為系統保留記憶體;

2.4 守護執行緒

守護執行緒是特殊的執行緒,一般用於處理後臺工作,如JDK垃圾回收;

2.4.1 JVM程式什麼情況下會退出

官方文件:若JVM中沒有一個非守護執行緒,則JVM的程序會退出;

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(
                () -> {
                    int i = 1;
                    try {
                        while (true) {
                            Thread.currentThread().setName("weixx");
                            //1當前執行緒sleep 10s
                            TimeUnit.SECONDS.sleep(10l);
                            System.out.println("thread 第[" + i + "]sleep結束~");
                            i++;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        System.out.println("thread 掛了~");
                    }
                }
        );
        //2 設定為守護執行緒
        thread.setDaemon(true);
        //3 啟動執行緒
        thread.start();
        //4 main方法執行緒sleep 5s
        TimeUnit.SECONDS.sleep(5l);
        System.out.println("main方法生命週期結束~");
    }

這裡寫圖片描述
發現main方法執行緒結束後,JVM也沒有退出,因為JVM程序中還存在一個非守護執行緒在執行;
現將2初的註釋開啟,將thread設定為守護執行緒,再次執行程式:
這裡寫圖片描述
thread第一次sleep結束後,JVM直接退出了,連日誌都沒來得及列印,因為沒有一個非守護執行緒存在了,所以退出了;

2.4.2 守護執行緒總結

守護執行緒特性:

  • 守護執行緒具備自動結束生命週期的特性
  • 主要用於一些後臺任務

參考文獻:
[ 1 ] Java高併發程式設計詳解 汪文君著。–北京:機械工業出版社,2018年6月第1版