天天看點

Java學習筆記 11、快速入門多線程(詳細)

文章目錄

  • ​​前言​​
  • ​​一、多線程基本認識​​
  • ​​1、程式、程序、線程​​
  • ​​2、認識單核與多核CPU​​
  • ​​3、多線程優點​​
  • ​​4、一個以上的執行空間說明​​
  • ​​二、線程的建立與使用​​
  • ​​認識Thread類​​
  • ​​兩種建立線程方式​​
  • ​​1、建立線程方式一:繼承Thread​​
  • ​​2、建立線程方式二:實作Runnable接口​​
  • ​​比較兩種建立方式​​
  • ​​常用方法​​
  • ​​修改線程名​​
  • ​​yield()方法​​
  • ​​join()方法​​
  • ​​sleep()方法​​
  • ​​線程優先級設定​​
  • ​​介紹排程​​
  • ​​線程優先級​​
  • ​​線程的分類​​
  • ​​三、線程的生命周期​​
  • ​​Thread.State中的六種狀态​​
  • ​​生命周期中五種狀态​​
  • ​​四、線程的同步​​
  • ​​1、多視窗賣票(引出問題)​​
  • ​​繼承Thread與實作Runnable接口兩種方式​​
  • ​​問題描述以及解決方案​​
  • ​​2、同步機制(解決線程安全問題)​​
  • ​​同步機制介紹​​
  • ​​方式一:同步代碼塊​​
  • ​​方式二:同步方法​​
  • ​​方式三:Lock鎖​​
  • ​​3、同步方法的好處及壞處​​
  • ​​4、同步的範圍及釋放與不釋放鎖的操作​​
  • ​​5、小練習​​
  • ​​五、線程死鎖問題​​
  • ​​1、介紹死鎖問題及執行個體情況​​
  • ​​2、解決與避免死鎖​​
  • ​​六、線程的通信​​
  • ​​1、認識線程通信​​
  • ​​2、線程通信小例子(交替列印1-100)​​
  • ​​不使用wait()、notify()實作線程通信(不推薦)​​
  • ​​使用wait()、notify()實作線程通信(推薦)​​
  • ​​3、經典例題(生産者與消費者)​​
  • ​​七、JDK5.0新增線程建立方式​​
  • ​​方式一:實作Callable接口​​
  • ​​方式二:使用線程池​​
  • ​​認識線程池的相關API​​
  • ​​執行個體:使用線程池建立10個線程來執行指定方法​​
  • ​​如何使用線程池的屬性?​​
  • ​​相關面試題​​
  • ​​1、synchronized與Lock 的對比​​
  • ​​2、sleep()與wait()方法異同點​​
  • ​​參考文章​​

前言

      去年四月份大一下半學期正式開始學習Java,一路從java基礎、資料庫、jdbc、javaweb、ssm以及Springboot,其中也學習了一段時間資料結構。

      在javaweb期間做了圖書商城項目、ssm階段做了權限管理項目,springboot學了之後手癢去b站看視訊做了個個人部落格項目(已部署到伺服器,正在備案中)。期間也不斷進行做筆記,總結,但是越學到後面越感覺有點虛,覺得自己基礎還有欠缺。

      之後一段時間我會重新回顧java基礎、學習一些設計模式,學習多線程并發之類,以及接觸一些jvm的相關知識,越學到後面越會感覺到基礎的重要性,之後也會以部落格形式輸出學習的内容。

      現在整理的java知識基礎點是在之前學習尚矽谷java課程的筆記基礎之上加工彙總,部分圖檔會引用尚矽谷或網絡上搜集或自己畫,在重新回顧的過程中也在不斷進行查漏補缺,盡可能将之前困惑的點都解決,讓自己更上一層樓吧。

      部落格目錄索引:部落格目錄索引(持續更新)

一、多線程基本認識

1、程式、程序、線程

程式(program):指代完成指定任務并使用某種語言編寫的一組指令的集合,也指代一段靜态的代碼。

程序(process):程式的一次執行過程,也可以用一個正在運作的程式來表示程序。它有自己的一個生命周期,自身産生、存在與消亡的過程。

線程(thread):程序中可以細化為線程,我們平時使用的就是主線程,我們也可以開辟其他線程來幫我們并行做其他事。若一個程序同一時間并行執行多個線程,就是支援多線程的!

  • 線程作為排程和執行的機關,每個線程都有自己獨立的運作棧和程式計數器(pc),并且線程切換的開銷小。
  • 一個程序中的多個線程共享相同的記憶體單元/記憶體位址空間,從同一堆中配置設定對象,可以通路相同的變量和對象,但多個線程共享的系統資源可能就會帶來安全的隐患。

單線程與多線程見圖:

Java學習筆記 11、快速入門多線程(詳細)

2、認識單核與多核CPU

單核CPU:其實是一種假的多線程,因為在一個時間單元内,也隻能執行一個線程的任務。好比高度公路有多個車道但是隻有一個從業人員收費。由于CPU時間單元短,我們平時運作程式時也不會感覺出來。

多核CPU:能夠更好的發揮多線程的效率,現在我們一般電腦都是多核的,伺服器也是。

例如我的電腦是8核的,對于暫時學習做一些普通的項目都是夠得:此電腦-右擊管理—裝置管理器即可檢視

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Vyr2szlY-1612970769700)(C:\Users\93997\AppData\Roaming\Typora\typora-user-images\image-20210204215155208.png)]

補充知識點:一個Java應用程式例如java.exe,其實最少有三個線程如main()主線程,gc()垃圾回收線程,異常處理線程。一旦發生異常的話就會影響主線程。

并行:多個CPU同時執行多個任務,例如多個人同時做不同的事情。

并發:一個CPU(采用時間片)同時執行多個任務。比如秒殺項目,多個人做同一件事。

3、多線程優點

背景介紹:以單核CPU為例,隻使用單個線程先後完成多個任務(調用多個方法),肯定比用多個線程來完成用時更短,但為什麼仍需要使用多線程呢?

  • 單核時,采用多線程會比采用單線程會更慢,因為進行多線程的過程中需要來回不斷切換線程。
  • 多核時,采用多線程就會比采用單線程快了,此時不需要來回進行切換。

多線程優點:

  1. 提高應用程式的響應,尤其是在圖形化界面中更會使用到,增強使用者的體驗。
  2. 提高計算機系統CPU的使用率。
  3. 改善程式結構。将既長又複雜的程序分為多個線程,獨立運作,友善于了解與修改。

何時需要使用多線程:

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

4、一個以上的執行空間說明

《head first java 2.0》中的一個問題:有一個以上的執行空間代表什麼?

      當我們建立多個線程,有超過一個以上的執行空間時,看起來會像是有好幾件事情同時發生。實際上,隻有真正的多處理器才能夠同時執行多件事情(前面也提到了)。

      對于使用在​

​Java​

​中的線程可以讓它看起來好像同時都在執行中,實際上執行動作在執行空間中非常快速的來回交換,是以我們會有錯覺每項任務都在同時進行,這裡說個數字在100個毫秒内目前執行程式代碼會切換到不同空間上的不同方法。

這裡又有一個問題:Java在作業系統中也隻是個在底層作業系統上執行的程序。一旦輪到Java執行時,Java虛拟機會執行什麼?

  • 目前執行空間最上面的會執行。

書中的截圖:描述線程執行空間

Java學習筆記 11、快速入門多線程(詳細)

二、線程的建立與使用

認識Thread類

Java的JVM允許程式運作多個線程,jdk也提供了相應的API,​

​Thread​

​類

​Thread​

​類:

  • 我們想讓一個線程做指定的事情是要通過某個特定​

    ​Thread​

    ​​對象的​

    ​run()​

    ​​方法來完成操作的,将​

    ​run()​

    ​方法的主體成為線程體。
  • 想讓run()中的内容執行并不是直接調用run()方法,而是調用其​

    ​Start()​

    ​方法。
  • 為啥不調用run()方法呢?因為run()方法是我們進行重寫的,若是直接調用run()方法來啟動會當做普通方法調用的,那麼就是主線程來執行了;調用start()方法啟動線程,整個線程處于就緒狀态,等待虛拟機排程,執行run方法,一旦run方法結束,此線程終止。

​Thread​

​類的構造器:

  • ​Thread()​

    ​:建立新的Thread對象
  • ​Thread(String threadname)​

    ​:建立線程并指定線程執行個體名
  • ​Thread(Runnable target)​

    ​:指定建立線程的目标對象,它實作了Runnable接 口中的run方法
  • ​Thread(Runnable target, String name)​

    ​:建立新的Thread對象

常用方法:

  • ​void start()​

    ​:啟動線程,并執行對象的run()方法。
  • ​String getName()​

    ​:傳回線程的名稱。
  • ​void setName()​

    ​:設定該線程名稱。
  • ​static native Thread currentThread()​

    ​:靜态⽅法,傳回對目前正在執⾏的線程對象的引⽤。
  • ​static native void yield()​

    ​:表示放棄的意思,表示目前線程願意讓出對目前處理器的占用,需要注意就算目前線程調用其方法,程式在排程時,也還是可能會執行該線程的。
  • ​static native void sleep(long millis)​

    ​:指定millis毫秒數讓目前線程進行睡眠,睡眠的狀态是阻塞狀态。
  • ​final void join()​

    ​:線上程a中可以調用線程b的join()方法,此時線程a進入阻塞狀态,知道線程b執行完之後,a才結束阻塞狀态。内部調用的是Object的wait()方法。

兩種建立線程方式

1、建立線程方式一:繼承Thread

為什麼要繼承Thread類呢?我們想要讓其他線程執行自己的事情那麼就需要先繼承Thread類并重寫run()方法,這樣我們建立自定義線程類執行個體,調用start()方法即可。

  • 這個run()方法其實是Thread類實作​

    ​Runnable​

    ​接口裡的方法。

繼承Thread方式:

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0;i<100;i++){
            System.out.println(i);
        }
    }
}

public class Main {

    //本身main方法是主線程
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();//①虛拟機中建立一個線程  ②調用線程的run()方法

        //主線程下執行語句
        for(int i=0;i<50;i++)
            System.out.println("主線程中循環:"+i);
    }

}      
Java學習筆記 11、快速入門多線程(詳細)
  • 可以看到主線程與自己建立的線程進行互動執行任務

注意點:

  1. 在程式中兩個線程在不斷的切換執行,他們的列印順序不會一樣,run()方法由JVM調用,什麼時候調用執行過程控制都有作業系統的CPU排程(CPU排程政策,主要執行程序的順序)決定。
  2. 記住要使用run()方法需要調用start()方法,若是調用run(),那麼就隻是普通方法,并沒有啟動多線程。
  3. 一個線程對象隻能調用一次start()方法啟動,如果重複調用了,會抛異常​

    ​IllegalThreadStateException​

    ​。當調用了start()後,虛拟機jvm會為我們建立一個線程,然後等這個線程第一次得到時間片再調用run()方法。

還可以使用Thread的匿名實作類來建立線程:

//本身main方法是主線程
public static void main(String[] args) {
    //使用Java8的函數式程式設計
    new Thread(()->{
        System.out.println("Thread的匿名實作類");
    }).start();

    //普通重寫方法
    new Thread(){
        @Override
        public void run() {
            System.out.println("匿名實作類");
        }
    }.start();
}      

2、建立線程方式二:實作Runnable接口

通過實作Runnable接口方式來建立對象,實作該接口的類作為參數傳遞到構造器中:

class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("實作Runnable接口");
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //方式一:通過實作接口類,并建立執行個體作為參數放置到Thread類中
        Thread thread = new Thread(new MyRunnable());
        thread.start();

        //方式二:通過給Thread類傳入匿名接口類
        new Thread(new MyRunnable(){
            @Override
            public void run() {
                System.out.println("匿名接口類runnable");
            }
        }).start();

    }
}      
  • 這裡示範了兩種使用實作Runnable接口方式來建立線程

源碼解析:對于非繼承方式實作Runnable接口在進行start()時進行調用作為參數中的run()方法

//構造器傳入接口實作類,采用多态方式,調用了init()方法将target傳入
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

//init()方法
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ....
}

//當調用start()方法實際上也會調用run()方法,看一下實際上就是調用的target.run()方法也就是之前傳入的target
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}      

對于實作​

​Runnable​

​接口來建立多個線程方式:

class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("實作Runnable接口");
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        //若是要多線程調用同一個接口實作類的run()方法,就需要建立多個Thread執行個體
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}      

比較兩種建立方式

對于線程中共享的屬性聲明介紹:

  • 繼承​

    ​Thread​

    ​類實作的,想要共享其中的屬性,那麼需要使用static來進行修飾
  • 實作​

    ​runnable​

    ​接口的,隻要是new Thread(runnable)中的runnable實作類是同一個時,其中屬性預設就是共享的。

那麼哪個方式我們更常使用呢?

開發中優先選擇實作​

​runnable​

​接口的方式

  • 實作方式沒有類的單繼承局限性。
  • 實作方式更适合來處理多個線程有共享資料的情況。
  • 降低了線程對象和線程任務的耦合性。

常用方法

修改線程名

//方式一:使用setName(threadname)方法更改,需要建立執行個體之後
new Thread(){
    ...run()
}.setName("線程一");

//方式二:建立執行個體時,使用有參構造器修改線程名 Thread(String name)
new Thread("線程一")
    
//方式三:雙參構造(實作runnable接口情況下)
new Thread(myRunnable, "線程1");      

修改了線程名,我們總要輸出線程名吧:

class MyThread extends Thread{
    @Override
    public void run() {
        //方式一:線上程中直接調用getName()
        System.out.println(this.getName());
    }
}

public class Main {
    public static void main(String[] args) {
        new MyThread().start();
        //方式二:通過Thread的靜态方法currentThread()擷取目前線程執行個體,調用方法即可
        System.out.println(Thread.currentThread().getName());
    }
}      

yield()方法

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%4==0){
                //使用該方法會交出目前cpu的占用,讓其他線程執行
                yield();
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        new MyThread().start();

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}      
  • 這裡測試的是當i%4==0時讓出占用的cpu資源,那麼就會執行其他線程了
Java學習筆記 11、快速入門多線程(詳細)

注意:使用yield()方法并不是每一次都有效的,也有可能繼續執行目前線程,了解含義即可。

join()方法

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

public class Main {
    public static void main(String[] args) {
        MyThread mythread = new MyThread();
        mythread.start();

        for (int i = 0; i < 10; i++) {
            if(i == 5){
                try{
                    //此時主線程阻塞,開始執行b線程,b線程執行完以後,主線程阻塞結束
                    mythread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}      
  • 當i==5時,我們讓主線程阻塞,來執行指定線程中内容,當指定線程執行完,主線程阻塞結束繼續執行
Java學習筆記 11、快速入門多線程(詳細)

sleep()方法

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if(i%2 == 1){
                try {
                    //讓該線程睡眠(阻塞)1秒
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread mythread = new MyThread();
        mythread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}      
  • 這裡是當i%2==0時該線程進入睡眠(阻塞)1秒。
Java學習筆記 11、快速入門多線程(詳細)

注意點:sleep()是Thread類中靜态方法,在哪個線程中調用該方法哪個線程就會進入睡眠,就算你在a線程中調用b線程的sleep()也還是a線程進入睡眠。

線程優先級設定

介紹排程

CPU通過為每個線程配置設定CPU時間⽚來實作多線程機制,對于作業系統配置設定時間片給每個線程的涉及到線程的排程政策。

Java學習筆記 11、快速入門多線程(詳細)
  • 搶占式:高優先級的線程搶占CPU

針對于Java的排程方法:

  • 同優先級線程組成先進先出隊列(先到先服務),使用時間片政策
  • 對于高優先級,使用優先排程的搶占式政策

線程優先級

Java中線程的優先級等級如下,定義在​

​Thread​

​類中的靜态屬性:

  • ​MAX_PRIORITY​

    ​:10
  • ​MIN _PRIORITY​

    ​:1
  • ​NORM_PRIORITY​

    ​:5

設定與擷取目前優先級方法:

  • ​setPriority(int newPriority)​

    ​ :改變線程的優先級
  • ​getPriority()​

    ​ :傳回線程優先值
//優先值可以直接使用Thread類中的靜态變量
myThread.setProority(Thread.MAX_PRIORITY);      

對于優先級的說明:

  1. 線程建立時繼承父線程的優先級
  2. 低優先級隻是獲得排程的機率低,并非一定是在高優先級線程之後才調用。

線程的分類

Java中的線程分為兩類:守護線程與使用者線程

  • 使用者線程:我們平常建立的普通線程。
  • 守護線程:用來服務于使用者線程,不需要上層邏輯介入。當線程隻剩下守護線程時,JVM就會退出,若還有其他使用者線程在,JVM就不會退出。

這兩種線程幾乎是一樣的,唯一的差別是判斷JVM何時離開!

如何設定線程為守護線程呢?

  • 在調用start()方法前,調用​

    ​Thread.setDaemon(true)​

    ​。就可以把一個使用者線程變成一個守護線程。

java垃圾回收就是一個典型的守護線程,若是jvm中都是守護線程時,JVM就會退出。

守護線程應用場景:

  1. 在任何情況下,程式結束時,這個線程必須正常且立刻關閉,就可以作為守護線程來使用,免去了有些時候主線程結束子線程還在執行的情況,當jvm隻剩下守護程序時,JVM就會退出。
  2. 相對于使用者線程,通常都是些關鍵的事務,這些操作不能中斷是以就不能使用守護線程。

三、線程的生命周期

Thread.State中的六種狀态

線程也具有生命周期,在JDK中使用​

​Thread.State​

​類來定義線程的幾種狀态,下圖是六種狀态:

Java學習筆記 11、快速入門多線程(詳細)
  • ​NEW​

    ​(初始):新建立的一個線程對象,還沒有調用start()。
  • ​RUNNABLE​

    ​(運作):Java線程中将就緒(ready)和運作中(running)兩種狀态籠統稱為"運作"。若調用了satrt()方法,該狀态線程位于可運作線程池中,等待被線程排程選中,擷取CPU使用權限,此時處于就緒狀态(ready)。就緒狀态線程獲得CPU時間片後變為運作中狀态(running)。
  • ​BLOCKED​

    ​(阻塞):線程處于阻塞狀态,等待監視鎖。
  • ​WAITING​

    ​(等待):在該狀态下的線程需要等待其他線程做出一些特定動作(通知或中斷)。
  • ​TIMED_WAITING​

    ​(逾時等待):調用sleep()、join()、wait()方法可能導緻線程處于等待狀态,不同于WAITING,它可以在指定時間後自行傳回。
  • ​TERMINATED​

    ​(終止):表示線程執行完畢。
參考文章:Java線程的6種狀态及切換(透徹講解)

詳細說明:

  1. 初始狀态(NEW)
  • 實作Runnable接口和繼承Thread可以得到一個線程類,new一個執行個體出來,線程就進入了初始狀态。
  1. 就緒狀态(RUNNABLE之READY)
  1. 就緒狀态隻是說你資格運作,排程程式沒有挑選到你,你就永遠是就緒狀态。
  2. 調用線程的start()方法,此線程進入就緒狀态。
  3. 目前線程sleep()方法結束,其他線程join()結束,等待使用者輸入完畢,某個線程拿到對象鎖,這些線程也将進入就緒狀态。
  4. 目前線程時間片用完了,調用目前線程的yield()方法,目前線程進入就緒狀态。
  5. 鎖池裡的線程拿到對象鎖後,進入就緒狀态。
  1. 運作中狀态(RUNNABLE之RUNNING)
  • 線程排程程式從可運作池中選擇一個線程作為目前線程時線程所處的狀态。這也是線程進入運作狀态的唯一的一種方式。
  1. 阻塞狀态(BLOCKED)
  • 阻塞狀态是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(擷取鎖)時的狀态。
  1. 等待(WAITING)
  • 處于這種狀态的線程不會被配置設定CPU執行時間,它們要等待被顯式地喚醒,否則會處于無限期等待的狀态。
  1. 逾時等待(TIMED_WAITING)
  • 處于這種狀态的線程不會被配置設定CPU執行時間,不過無須無限期等待被其他線程顯示地喚醒,在達到一定時間後它們會自動喚醒。
  1. 終止狀态(TERMINATED)
  • 當線程的run()方法完成時,或者主線程的main()方法完成時,我們就認為它終止了。這個線程對象也許是活的,但是它已經不是一個單獨執行的線程。線程一旦終止了,就不能複生。
  • 在一個終止的線程上調用start()方法,會抛出java.lang.IllegalThreadStateException異常。

生命周期中五種狀态

Java語言使用​

​Thread​

​類及其實作類的對象來建立使用線程,完整的生命周期中通常要經曆如下的五種狀态:

​建立—就緒—運作—阻塞—死亡​

  • 建立:當一個Thread類或其子類的對象被聲明并被建立時,新生的線程對象處于建立狀态。
  • 就緒:處于建立狀态的線程被​

    ​start()​

    ​後,将進入線程隊列等待CPU時間片,此時它已具備了運作的條件,隻是沒配置設定到CPU資源。
  • 運作:當就緒的線程被排程并獲得CPU資源時,便進入運作狀态,​

    ​run()​

    ​方法定義了線程的操作和功能。
  • 阻塞:在某種特殊情況下,被人為挂起或執行輸入輸出操作時,讓出 CPU 并臨時中止自己的執行,進入阻塞狀态。
  • 死亡:線程完成了它的全部工作(run方法結束)或線程被提前強制性地中止或出現異常導緻結束。

周期圖如下:

Java學習筆記 11、快速入門多線程(詳細)

四、線程的同步

1、多視窗賣票(引出問題)

繼承Thread與實作Runnable接口兩種方式

多視窗賣票問題描述:我們在run()方法中模拟賣票的過程,一旦進入while第一條輸出語句表示售出一張票,在多線程中若是進行就可能會出現下面的賣出重複票、錯票的問題。

繼承Thread的多視窗賣票問題
class MyThread extends Thread{

    //繼承Thread方式想要共享屬性:設定為static
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
                ticket--;
            }else{
                break;
            }
        }
    }

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

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread("線程一");
        MyThread thread2 = new MyThread("線程二");
        thread1.start();
        thread2.start();
    }
}      
  • 使用繼承Thread方式建立線程想要共享屬性就需要設定static靜态,因為都是new的同一個類的對象。
Java學習筆記 11、快速入門多線程(詳細)
實作Runnable接口的多視窗賣票問題
class MyRunnable implements Runnable{

    //實作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 Main {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        //使用相同的接口實作類runnable
        Thread thread1 = new Thread(runnable, "線程一");
        Thread thread2 = new Thread(runnable, "線程二");
        thread1.start();
        thread2.start();
    }
}      
  • 在實作runnable接口類中對于想要共享的屬性不需要設定static,因為該類建立的執行個體都作為Thread構造器參數傳入,使用的是一個runnable。
Java學習筆記 11、快速入門多線程(詳細)

問題描述以及解決方案

問題:在賣票過程中,出現了重票、錯票,出現了線程的安全問題。當某個線程操作車票的過程中,尚未操作完成時,其他操作與此同時參與進來執行導緻共享資料的錯誤。

解決辦法:對于多條操作共享資料的語句,隻能讓一個線程都執行完,在執行過程中其他線程不可以參與執行。

2、同步機制(解決線程安全問題)

同步機制介紹

同步機制

同步機制:Java對于線程安全問題也出同步機制,當線程調用一個方法時在沒有得到結果之前,其他線程無法參與執行。

有兩種方式解決:

  • 同步代碼塊:​

    ​synchronized(同步螢幕){ ...操作共享資料 }​

    ​,單獨在方法中聲明
  • 同步方法:​

    ​public synchronized void show (String name){ ​

    ​,直接聲明在方法上
synchronized介紹

​synchronized​

​鎖是什麼?

  1. 任意對象都可以作為同步鎖,所有對象都自動含有單一的鎖(螢幕)。
  2. 同步代碼塊:同步螢幕可以是單一的對象,很多時候可以将this或類.class作為鎖。
  3. 同步方法的鎖:可以看到隻需要在方法權限修飾前加入synchronized即可,它對應的鎖是根據它的方法狀态,若其方法是static靜态方法(​

    ​類.class​

    ​​);非靜态方法(​

    ​this​

    ​)

synchronized充當角色如圖:

Java學習筆記 11、快速入門多線程(詳細)

同步鎖機制:對于并發工作,你需要某種方式來防 止兩個任務通路相同的資源(其實就是共享資源競争)。 防止這種沖突的方法 就是當資源被一個任務使用時,在其上加鎖。第一個通路某項資源的任務必須 鎖定這項資源,使其他任務在其被解鎖之前,就無法通路它了,而在其被解鎖之時,另一個任務就可以鎖定并使用它了。 —《Thinking in Java》

注意:

  1. 必須要確定使用同一個資源的多個線程共用一把鎖,否則無法保證操縱共享資源的安全。
  2. 針對于同步方法中一個線程類中的所有靜态方法都會共用同一把鎖(​

    ​類.class​

    ​​),非靜态方法都會使用​

    ​this​

    ​充當鎖;同步代碼塊一定要謹慎。

方式一:同步代碼塊

文法:

synchronized(同步螢幕){ 
  //需要被同步的代碼
}      
  • 共享資料:多個線程共同操作的變量,例如之前的ticket。
  • 被同步代碼:操作共享資料的代碼。
  • 同步螢幕:俗稱鎖,可以是任意一個對象,都可以充當鎖,多個線程必須共用一把鎖。

解決之前兩種方式進行多線程的線程安全問題:

  1. 繼承Thread方式
class MyThread extends Thread{

    private static int ticket = 100;

    //這裡單獨建立一個靜态對象
    private static Object object = new Object();

    @Override
    public void run() {
        while(true){
            //這裡使用Object靜态執行個體對象作為鎖
            synchronized (object){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }

    public MyThread(String name) {
        super(name);
    }
}      
  • 這裡的鎖是使用的類中一個靜态對象執行個體object,對于這種繼承Thread方式進行多線程的話是要new多個​

    ​MyThread​

    ​類的,而Synchronized中的鎖必須是指定單獨一把鎖,是以将該object作為鎖。
  1. 實作runnable接口
//解決實作runnable接口的線程安全問題
class MyRunnable implements Runnable{
    
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            //同步代碼塊:this本身指的是runnable的實作類MyRunnable
            synchronized (this){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }

}      
  • 對于實作runnable方式的,我們可以直接使用​

    ​this​

    ​來充當鎖,因為建立多個線程使用的是同一個Runnable。

注意:不管什麼形式來建立多線程的,若是想要避免出現線程安全問題那麼就要確定synchronized裡的鎖是一把鎖也就是同一個對象。

方式二:同步方法

文法:

//非靜态方法:鎖為this
public synchronized void show(){
}

//靜态方法:鎖為類.class
public synchronized static void show(){
}      

解決之前兩種方式進行多線程的線程安全問題:

  1. 繼承Thread方式
class MyThread extends Thread{

    //繼承Thread方式想要共享屬性:設定為static
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket<=0 || operate()){
                break;
            }
        }
    }

    //同步方法:靜态方法 鎖為MyThread.Class 單獨一份
    public synchronized static boolean operate(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
            ticket--;
            return ticket==0;
        }else{
            return true;
        }
    }

    public MyThread(String name) {
        super(name);
    }
}      
  • 這裡就需要使用靜态方法了,此時鎖為​

    ​MyThread.class​

    ​單獨一份,若是非靜态的話就是this,而下面建立多線程就是多個執行個體,就會出現安全問題。
  1. 實作Runnable接口方式
class MyRunnable implements Runnable{

    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            //一旦ticket<=0 馬上退出
            if(ticket<=0 || operate()){
                break;
            }
        }
    }

    //非靜态方法使用this來作為鎖
    public synchronized boolean operate(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
            ticket--;
            return ticket==0;
        }else{
            return true;
        }
    }

}      
  • 這裡将判斷操作單獨抽成一個同步方法,這裡是非靜态的,鎖就是​

    ​this​

注意:繼承Thread方式中應使用靜态方法來實作同步方法;實作runnable接口實作同步方法時,該方法是否為靜态都可以,因為多個線程始終使用的是一個runnable,鎖始終隻有一份。

方式三:Lock鎖

認識Lock鎖

從JDK5.0開始,Java提供了更強大的線程同步機制—使用顯示定義同步鎖對象來實作同步,同步鎖使用​

​Lock​

​對象充當。

該Lock接口是屬于​

​java.util.concurrent.locks​

​包,它用來控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前應先獲得Lock對象。

如何使用Lock接口呢?

  • 我們使用其Lock接口實作類​

    ​ReentrantLock​

    ​類(重進入鎖)。
  • ​ReentrantLock​

    ​​類:它擁有與synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是​

    ​ReentrantLock​

    ​​類,可以調​

    ​lock()​

    ​​方法顯式加鎖,調​

    ​unlock()​

    ​方法釋放鎖。
實際使用:解決線程同步

解決之前兩種方式進行多線程的同步問題:

  1. 解決繼承​

    ​Thread​

    ​的同步問題
class MyThread extends Thread{

    //繼承Thread方式想要共享屬性:設定為static
    private static int ticket = 100;
    //靜态執行個體:使用Lock鎖
    private static Lock lock = new ReentrantLock();

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

            //手動上鎖
            lock.lock();

            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
                ticket--;
            }else{
                lock.unlock();
                break;
            }

            //手動解鎖
            lock.unlock();
        }
    }
}      
  • 針對于繼承Thread方式的lock鎖在建立執行個體時定義為​

    ​static​

    ​。之後再在操作共享資料外顯示上鎖與解鎖。
  1. 解決實作Runnable接口同步問題
class MyRunnable implements Runnable{

    //實作runnable接口方式想要共享屬性:預設權限
    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            //上鎖
            lock.lock();
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+"的票數出售:"+ticket);
                ticket--;
            }else{
                lock.unlock();
                break;
            }
            //解鎖
            lock.unlock();
        }
    }
}      
  • 針對于實作Runnable的情況,我們就建立一個普通執行個體對象即可。多個線程使用的是同一個Runnable實作類。

注意:如果同步代碼有異常,要将​

​unlock()​

​寫入finally語句塊。

3、同步方法的好處及壞處

好處:解決了線程的安全問題。

壞處(局限性):在操作同步代碼時,隻能有一個線程參與,其他線程需要等待開鎖之後才能進入,相當于是一個單線程的過程,效率低。

4、同步的範圍及釋放與不釋放鎖的操作

同步範圍

對于尋找代碼是否存線上程安全問題幾個關鍵點?

  • 明确哪些方法是多線程運作的代碼。
  • 明确多個線程是否有共享資料。
  • 明确多線程代碼中是否有多條語句操作共享資料。

解決政策:對于多條操作共享資料的語句,隻能讓一個線程執行完之後再讓下個線程執行,即所有操作共享資料的這些語句都要放在同步範圍中。對于同步範圍的大小也要有個度,範圍小太的話往往可能會沒有鎖住所有安全的問題;範圍太大的話沒發揮多線程的功能。

釋放鎖與不會釋放鎖的操作

釋放鎖的操作:

  • 目前線程的同步方法或同步代碼塊執行結束會自動釋放鎖。
  • 目前線程在同步方法或同步代碼塊中遇到​

    ​break​

    ​​、​

    ​return​

    ​終止了該代碼塊、該方法的執行。
  • 目前線程在同步方法或同步代碼塊中出現了未處理的​

    ​Error​

    ​​或​

    ​Exception​

    ​,導緻異常結束。
  • 目前線程在同步方法或同步代碼塊中執行了線程對象的​

    ​wait()​

    ​方法,目前線程暫停,并釋放鎖。

不會釋放鎖的操作:

  • 線程執行同步方法或同步代碼塊時,程式調用​

    ​Thread.sleep()​

    ​​、​

    ​Thread.yield()​

    ​方法暫停目前線程的執行。
  • 其他線程調用了該線程的​

    ​suspend()​

    ​方法将該線程挂起,該線程不會釋放鎖(同步螢幕)
  • ​suspent()​

    ​​與​

    ​resume()​

    ​方法已棄用。

5、小練習

問題描述:銀行有一個賬戶。 有兩個儲戶分别向同一個賬戶存3000元,每次存1000,存3次。每次存完打 印賬戶餘額。

程式如下:

class Account{
    private double wallet;

    //将該方法設定為同步方法,鎖為this指的是該Account類,這裡是可以的
    public synchronized void deposit(double money){
        if(wallet>=0){
            wallet += money;
            System.out.println(Thread.currentThread().getName()+"向賬戶存儲了"+money+"元,賬戶餘額為:"+wallet);
        }
    }
}

class Customer extends Thread{

    private Account account;

    public Customer(Account account,String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account();
        new Customer(account,"使用者A").start();
        new Customer(account,"使用者B").start();
    }
}      
  • 這裡直接将Account操控資料的方法設定為同步方法,不需要使用static,因為多個線程共同操作一個Account
Java學習筆記 11、快速入門多線程(詳細)

五、線程死鎖問題

1、介紹死鎖問題及執行個體情況

死鎖:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,是以程式不可能正常終止。

  • Java語言通過synchronized關鍵字來保證原子性,其中每一個Object都有一個隐含鎖,這個也稱為螢幕對象,在進入到synchronized之前自動擷取此内部鎖,一旦離開此方式會自動釋放鎖。

使用1個例子來描述死鎖如何形成:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        StringBuffer buffer1 = new StringBuffer();
        StringBuffer buffer2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (buffer1){
                    buffer1.append("A");
                    buffer2.append("A");

                    //睡眠2秒
                    sleep2Sec();

                    synchronized (buffer2){
                        buffer1.append("B");
                        buffer2.append("B");
                    }
                }
                System.out.println("線程1中:buffer1="+buffer1);
                System.out.println("線程1中:buffer2 = "+buffer2);
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                synchronized (buffer2){
                    buffer1.append("C");
                    buffer2.append("C");

                    //睡眠2秒
                    sleep2Sec();

                    synchronized (buffer1){
                        buffer1.append("D");
                        buffer2.append("D");
                    }
                }
                System.out.println("線程2中:buffer1="+buffer1);
                System.out.print("線程2中:buffer2 = "+buffer2);
            }
        }.start();
    }

    //延時2秒
    public static void sleep2Sec(){
        //增加延時
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}      
  • 注意看兩個線程中的run()方法裡有兩個synchronized同步代碼塊,第一個線程run()中第一個同步代碼塊将buffer1作為鎖,第二個線程的第一個同步代碼塊将buffer2對象作為鎖,兩個同步代碼塊的内部同步代碼塊也是各自設定buffer1或buffer2為鎖。
  • 為了造出死鎖的情況,在兩個線程都進入到第一個同步代碼塊時都使用​

    ​sleep()​

    ​來讓線程阻塞一會(不會釋放鎖),待兩個線程都同時進入到第一個同步代碼塊中,第一個線程拿到buffer1内部鎖,第二個線程拿到buffer2内部鎖,一旦sleep()結束阻塞,那麼就會出現死鎖狀況,各個都在等待對方資源被釋放。
Java學習筆記 11、快速入門多線程(詳細)

即不斷的在阻塞中…

這個例子是因為多線程通路共享資源由于通路順序原因所造成阻塞情況,一個線程鎖住資源A,由想去鎖住資源B;在另一個線程中先鎖住B,又想鎖住A來完成操作,一旦兩個線程同時先後鎖住A與B時,就會造成兩個線程都在等待情況,程式進入阻塞。

2、解決與避免死鎖

通過專門的算法,盡量避免同不資源定義以及避免嵌套同步。

這裡介紹三個技術避免死鎖問題:

  1. 加鎖順序(線程按照一定的順序上鎖)
  2. 加鎖時限(線程嘗試擷取鎖時加上一定時限,超過時限則放棄對該鎖請求,并釋放自己所占有鎖)
  3. 死鎖檢測
方式一:加鎖順序
//第一個線程
synchronized (buffer1){
    ....

    synchronized (buffer2){
        .....
    }
//第二個線程
synchronized (buffer1){
    ....

    synchronized (buffer2){
        .....
    }      

說明:對上鎖的順序作适當排序,這樣就不會進入到死鎖情況,因為無論哪個線程先進入,另一個線程會一直等待鎖的釋放,直到第一個使用該鎖的釋放再進行。

方式二:加鎖時限

介紹:就是在擷取鎖的時候加一個逾時時間,一旦超過了這個時限則會放棄該鎖的請求,并釋放自己所占用的鎖。

在Java中不能對synchronized同步塊設定逾時時間。你需要建立一個自定義鎖,或使用​

​java.util.concurrent​

​包下的工具

六、線程的通信

1、認識線程通信

引出線程通信

為什麼要線程通信?

  • 多個線程并發執行時預設是根據CPU排程政策随機分發時間片,對于任務的執行其實是随機的,當我們需要多線程來共同完成一件事,并且希望它能夠有規律的執行,那麼就需要一些協同通信,來達到多線程共同操縱一份資料。
  • 多線程中若是我們不使用線程通信的方式也是可以實作共同完成一件事,但是在很大程度上多線程會對共享變量進行争奪造成損失,是以引出線程通信,目的是能讓多線程之間的通信避免同一共享資源的争奪。

什麼是線程通信?

  • 多個線程在處理同一個共享變量,且任務不同時需要線程通信來解決對一個變量使用與操作的随機性,使其變得有規律,具有可控性,避免對同一共享變量進行争奪。
  • 想要實作線程通信這裡就引出等待喚醒機制,如​

    ​wait()​

    ​​、​

    ​notify()​

    ​​、​

    ​notifyAll()​

    ​方法。

認識三個方法,三個方法都是Object對象提供。

Object聲明三個方法原因:這三個方法必須由鎖對象調用,而任何對象都可以作為synchronized的同步鎖。

前提:三個方法隻有在​

​synchronized方法​

​​或​

​synchronized代碼塊​

​​中才能使用,否則會報​

​IllegalMonitorStateException​

​異常(如果目前線程不是此對象的螢幕所有者)。

​wait()​

​:讓目前線程挂起并放棄CPU、同步資源并等待,使别的線程可通路并修改共享資源,此時目前線程會進行排隊等候其他線程調用notify()與notifyAll()方法喚醒,喚醒後等待重新獲得對螢幕的所有權後才能夠執行。

  • 簡述:該線程暫停等待,釋放此螢幕的所有權(指鎖),等待指定方法喚醒重新獲得螢幕所有權,從斷點處開始執行。

​notify()​

​:喚醒正在排隊等待同步資源的線程中優先級最高者結束等待。

  • 簡述:喚醒正在等待對象螢幕的單個線程,如之前使用wait()方法的線程,需要注意不能喚醒sleep()的線程。

​notifyAll()​

​:喚醒正在排隊等待資源的所有線程結束等待。

  • 簡述:喚醒所有等待對象螢幕的線程,如通過調用wait()方法之一等待對象的螢幕,非sleep()方法。

注意:

  1. 這幾個方法配合使用需要使用同一個對象來進行調用。
  2. 調用方法的必要條件:目前線程必須具有對該對象的監控權(加鎖)。

2、線程通信小例子(交替列印1-100)

不使用wait()、notify()實作線程通信(不推薦)

這裡僅使用繼承​

​Thread​

​方式來實作線程通信:

class MyThread extends Thread{

    private static int i;

    @Override
    public void run() {
        while(true){
            synchronized (MyThread.class){
                //指定隻有偶數情況且目前線程為線程一時才執行
                if(i<100 && i%2 == 0){
                    if(Thread.currentThread().getName() == "線程一"){
                        i++;
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }else if(i<100 && i%2 == 1){//指定隻有奇數情況且目前線程為線程二時才執行
                    if(Thread.currentThread().getName() == "線程二"){
                        i++;
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }

                if(i>=100){
                    break;
                }
            }

        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.setName("線程一");
        thread2.setName("線程二");
        thread1.start();
        thread2.start();
    }
}      
  • 通過雙重判斷來達到線程交替列印,不過這種方式會有大量無效情況以及可能會出現問題,更消耗資源。
Java學習筆記 11、快速入門多線程(詳細)

使用wait()、notify()實作線程通信(推薦)

【1】實作Runnable接口方式

class MyRunnable implements Runnable{

    private int i;

    @Override
    public void run() {
        while(true){
            //同步代碼塊
            synchronized (this){
                if(i<100){
                    //進行喚醒排隊等待螢幕的線程,此時繼續向下執行相應操作
                    this.notify();
                    i++;
                    System.out.println(Thread.currentThread().getName()+":"+i);

                    //進入等待
                    try {
                        if(i<100) //加一個判斷防止最後出現阻塞情況
                            this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread thread1 = new Thread(runnable, "線程一");
        Thread thread2 = new Thread(runnable, "線程二");
        thread1.start();
        thread2.start();
    }
}      
  • 這裡因為是實作runnable接口,在建立多個線程時使用的是同一個Runnable實作類,是以我們可以直接使用該對象作為螢幕。
Java學習筆記 11、快速入門多線程(詳細)

【2】繼承Thread方式

class MyThread extends Thread{

    private static int i;

    @Override
    public void run() {
        while(true){
            //将MyThread.class作為鎖,隻有一個類
            synchronized (MyThread.class){

                //作為鎖的類調用喚醒方法
                MyThread.class.notify();

                if(i<100){
                    i++;
                    System.out.println(Thread.currentThread().getName()+":"+i);

                    try {
                        if(i<100)
                            MyThread.class.wait();//釋放螢幕并進行等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.setName("線程一");
        thread2.setName("線程二");
        thread1.start();
        thread2.start();
    }
}      
  • 對于繼承Thread來實作多線程的,共享變量為static,這裡鎖為自定義類的class類
Java學習筆記 11、快速入門多線程(詳細)

3、經典例題(生産者與消費者)

介紹作業系統中的生産者與消費者

作業系統中的問題:系統中有一組生産者進行與一組消費者程序,生産者程序每次生産一個産品放入緩沖區,消費者程序每次從緩沖區中取出一個産品并使用(這裡"産品"了解為某種資料)。

①生産者、消費者共享一個初始為空、大小為n的緩沖區。

②隻有緩沖區沒滿時,生産者才能把産品放入緩沖區中,否則必須等待;

③隻有緩沖區不空時,消費者才能從中取出産品,否則必須等待。

④緩沖區是臨界資源,葛金城必須互斥地通路。

模拟生産者與消費者案例

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

出現問題描述:

  1. 生産者比消費者快時,消費者會漏掉一些資料沒有取到。
  2. 消費者比生産者快時,消費者會取相同的資料。

程式如下:

//店員類:有生産産品與消費産品功能
class Clerk{

    //設定初始産品為0,最高産品數量為20
    private int product;

    //生産産品
    public synchronized void addProduct(){
        //産品數量夠了
        if(product>=20){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            product++;//生産産品
            System.out.println("産品+1,目前産品數量為"+product);
            notify();//喚醒操作說明生産了新的産品了
        }

    }

    //消費産品
    public synchronized void consumeProduct(){
        //如果産品為0了,那麼無法進行消費
        if(product<=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            product--;
            System.out.println("産品-1,目前産品數量為"+product);
            notify();
        }
    }

}

//生産者
class Product extends Thread{

    private Clerk clerk;

    public Product(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        //不斷進行生産操作
        while(true){

            //為了讓效果更加明顯這裡對線程使用延時
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.addProduct();
        }
    }
}


//消費者
class Consumer extends Thread{

    private Clerk clerk;

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

    @Override
    public void run() {
        //不斷進行消費操作
        while(true){
            clerk.consumeProduct();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Clerk clerk = new Clerk();
        Product proThread = new Product(clerk);
        Consumer conThread = new Consumer(clerk);
        proThread.start();
        conThread.start();
    }
}      

程式分析:

​Clerk​

​店員類來負責生産産品與消費産品的進行

  • 生産方法:一旦産品數量>=20,則進入阻塞狀态(表示已滿無法生産);若<=20就進行生産,并喚醒使用wait()等待的消費者(通知它我生産出産品了你可以進行消費了)。
  • 消費方法:一旦産品數量<=0,則進入阻塞狀态(表示無産品暫時無法消費);若>0則進行消費,并喚醒使用wait()等待的生産者(告知它我已經消費産品了,快去生産)。

​Product​

​​作為生産者線程,​

​Consumer​

​作為消費者線程。

  • 這裡給生産者線程加了sleep()方法,生産速遞慢,消費速度快,展現的更加明顯。
Java學習筆記 11、快速入門多線程(詳細)

七、JDK5.0新增線程建立方式

方式一:實作Callable接口

介紹Callable接口

​Callable​

​接口:與使用Runnable相比, 其功能更加強大

  • 相比run()方法,其實作的callable接口方法​

    ​call()​

    ​中可以有傳回值
  • 方法可抛出異常。
  • 支援泛型的傳回值。
  • 需要借助​

    ​FutureTask​

    ​類的get()方法擷取call()的傳回值。
Java學習筆記 11、快速入門多線程(詳細)
案例示範

案例描述:使用多線程擷取到0-99和的值

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

class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 0;i<100;i++){
            sum+=i;
        }
        return sum;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyCallable callable = new MyCallable();
        //将MyCallable建立的執行個體放置到FutureTask的有參構造器中,futureTask實作了Run()方法,其中就調用了callable的call()方法
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //啟動線程
        new Thread(futureTask).start();

        try {
            //通過FutureTask類的get()方法調用擷取call()方法的傳回值
            Integer sum = futureTask.get();
            System.out.println(sum);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        
    }
}      
  • FutureTask實作了​

    ​RunnableFuture​

    ​​接口,并且該接口又多繼承了​

    ​Runnable​

    ​​,​

    ​Future<V> ​

    ​這兩個接口。
  • Future接口的get()方法:能夠擷取到實作​

    ​Callable​

    ​​的​

    ​call()​

    ​方法的傳回值。
  • 為什麼要将​

    ​MyCallable​

    ​​執行個體放到​

    ​FutureTask​

    ​中?
  • ​FutureTask​

    ​​中實作了Runnable接口,其中也包含run()方法,run()方法裡調用了實作Callable執行個體類中的​

    ​call()​

    ​​方法,并且在run()過程中擷取到了call()的傳回值,使用其中的​

    ​set()​

    ​​方法指派到自己類中屬性裡。是以我們下面也就可以看到使用其類的​

    ​get()​

    ​方法擷取到了call()的傳回值。
  • 為什麼将​

    ​FutureTask​

    ​執行個體放到Thread中?
  • 之前也說到了​

    ​FutureTask​

    ​​實作了runnable接口,符合Thread類中的一個有參構造,一旦調用start()就會執行​

    ​FutureTask​

    ​的run()方法。
Java學習筆記 11、快速入門多線程(詳細)
源碼分析一波

首先看一下​

​Callable​

​接口類:

//函數式接口,允許使用Lambda表達式
@FunctionalInterface
public interface Callable<V> {
    
    //支援自定義泛型傳回值,可以抛出異常
    V call() throws Exception;
}      

接着看​

​RunnableFuture​

​類:見下面1.1

//隻列舉Future<V>接口,runnable接口中隻有一個run()抽象方法這裡不展開
public interface Future<V> {
    ...
    V get() throws InterruptedException, ExecutionException;
}

//1.2 RunnableFuture接口 多繼承Runnable接口以及Future接口 
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

//1.1實作RunnableFuture接口 (1.2見RunnableFuture接口)
public class FutureTask<V> implements RunnableFuture<V> {
    //使用outcome來接收call()方法傳回值
    private Object outcome;
    
    //有參構造器,使用Callable多态
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    
    //看一下run()方法,之後Thread類中使用start()方法會調用該run()方法
    public void run() {
        ...
         Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //這裡調用了之前有參構造器中傳入的Callable接口實作類的call()方法,使用V來接收傳回值
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    //調用set()方法将傳回值指派到
                    set(result);
            }
        ....
    }
    
    //set()方法:本身類自己實作
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;//指派操作
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
    
    //通過調用get()方法擷取到傳回值:實作Future接口
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        //調用方法傳回
        return report(s);
    }
    
    //該方法用于傳回outcome的值也就是調用call()的傳回值
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
}      
  • 關注一下其中的run()方法以及get()方法,簡單來說該類中run()方法實際上就是調用了實作Callable類的call()方法,get()方法擷取到了call()方法的傳回值,具體内容見上。

最後看一下​

​Thread​

​的構造器:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}      
  • 由于​

    ​RunnableFuture​

    ​也實作了Runnable接口,是以能夠傳入到Thread的構造器中。

上面隻是粗略看了一下源碼找出了關鍵資訊,對于具體内容實作并沒有太過深入了解,僅大概方法調用也有了些思路。

方式二:使用線程池

認識線程池的相關API

線程池背景及好處

背景:經常建立和銷毀、使用量特别大的資源,比如并發情況下的線程, 對性能影響很大,我們可以使用現成的線程池。

思路:提前建立好多個線程,放入線程池中,使用時直接擷取,使用完放回池中。可以避免頻繁建立銷毀、實作重複利用。

好處:

  1. 提高響應速度(減少了建立新線程的時間)。
  2. 降低資源消耗(重複利用線程池中線程,不需要每次都建立)。
  3. 便于線程管理,例如:​

    ​corePoolSize​

    ​​:核心池的大小​

    ​maximumPoolSize​

    ​​:最大線程數​

    ​keepAliveTime​

    ​:線程沒有任務時最多保持多長時間後會終止。這些都可直接設定。
認識了解線程池相關API

同樣是​

​JDK5.0​

​​,提供了線程池的相關的API:​

​ExecutorService​

​​ 和​

​ Executors​

​ExecutorService​

​:真正的線程池接口。常見子類​

​ThreadPoolExecutor ​

  • ​void execute(Runnable command)​

    ​ :執行任務/指令,沒有傳回值,一般用來執行 Runnable。
  • ​Future submit(Callable task)​

    ​:執行任務,有傳回值,一般又來執行 Callable。
  • ​void shutdown() ​

    ​:關閉連接配接池。

​Executors​

​:工具類、線程池的工廠類,用于建立并傳回不同類型的線程池

  • ​ExecutorService Executors.newCachedThreadPool()​

    ​:建立一個可根據需要建立新線程的線程池
  • ​ExecutorService Executors.newFixedThreadPool(n)​

    ​: 建立一個可重用固定線程數的線程池
  • ​ExecutorService Executors.newSingleThreadExecutor() ​

    ​:建立一個隻有一個線程的線程池
  • ​ScheduledExecutorService Executors.newScheduledThreadPool(n)​

    ​:建立一個線程池,它可安排在給定延遲後運 行指令或者定期地執行。

說明:我們主要使用​

​Executors​

​​工具類來擷取到線程池,上面列舉到的前三個實際上傳回的是​

​ThreadPoolExecutor​

​這個實作類,ExecutorService是該實作類實作的接口。

我們想要執行我們自定義的線程任務就可以使用上面​

​ExecutorService​

​列舉到的方法。

Java學習筆記 11、快速入門多線程(詳細)

執行個體:使用線程池建立10個線程來執行指定方法

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

class MyRunnable implements Runnable{

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

public class Main {
    public static void main(String[] args) {
        //1、使用工具類建立10個線程
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //2、在将來某個時候執行給定的任務,這裡submit()方法需要提供Runnable接口實作類
        pool.submit(new MyRunnable());
        //3、啟動有序關閉,其中先前送出的任務将被執行,但不會接受任何新任務。
        pool.shutdown();

    }
}      
Java學習筆記 11、快速入門多線程(詳細)

如何使用線程池的屬性?

首先列舉三個屬性:

  • ​corePoolSize​

    ​:核心池的大小
  • ​maximumPoolSize​

    ​:最大線程數
  • ​keepAliveTime​

    ​:線程沒有任務時最多保持多長時間後會終止
Java學習筆記 11、快速入門多線程(詳細)
  • 我們需要向下轉型為ThreadPoolService才能調用指定方法
檢視源碼

首先看​

​Executors.newFixedThreadPool(10)​

​方法

//這裡實際上使用了多态,ExecutorService是ThreadPoolExecutor的接口
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}      

再其ExecutorService接口中并沒有設定線程池屬性的方法:

Java學習筆記 11、快速入門多線程(詳細)

是以我們需要向下轉型,這是允許的,因為在本身傳回的時候就是傳回的​

​ThreadPoolExecutor​

​執行個體對象。

相關面試題

1、synchronized與Lock 的對比

相同點:二者都可以解決線程安全問題

不同點:

  1. ​synchronized​

    ​機制再執行完相應的同步代碼以後,會自動的釋放同步螢幕。
  2. ​Lock​

    ​​鎖需要手動上鎖以及解鎖,結束同步需要手動調用​

    ​unlock()​

    ​方法。
  3. 使用​

    ​Lock​

    ​​鎖,​

    ​JVM​

    ​将會花費較少的時間來排程線程,性能會更好,并且具有更好的擴充性(提供了更多的子類)。

優先使用順序:​

​Lock​

​鎖 > 同步代碼塊(進入方法體配置設定了相應資源) -> 同步方法

2、sleep()與wait()方法異同點

相同點:這兩個方法都能夠讓線程進入到阻塞狀态。

不同點:

  1. 兩個方法聲明不同,sleep()方法聲明在Thread類中,wait()方法聲明在Object類中。
  2. 調用位置不同,sleep()方法在任何需要的場景下都可以使用,而wait()方法隻能在同步代碼塊或同步方法中使用。
  3. 關于是否釋放螢幕,sleep()不會釋放鎖,wait()會釋放鎖。

參考文章

[1]. 書籍《head first java 2.0》

[2]. ​​尚矽谷-Java30天-多線程篇(宋紅康主講)​​

[3]. 多線程:建立Thread為什麼要調用start啟動,而不能直接調用run方法

[4]. ​​什麼是CPU排程?​​

[5]. 主線程調用子線程對象的 sleep() 方法,會造成子線程睡眠嗎?

[6]. Java守護線程的了解和使用場景

[7]. ​​什麼是守護線程?​​

[8]. Java線程的6種狀态及切換(透徹講解)

[9]. Java多線程:死鎖

[10]. 線程通信的例子:使用線程交替列印1-100

[11]. 作業系統——生産者消費者問題

我是長路,感謝你的閱讀,如有問題請指出,我會聽取建議并進行修正。

歡迎關注我的公衆号:長路Java,其中會包含軟體安裝等其他一些資料,包含一些視訊教程以及學習路徑分享。

學習讨論qq群:891507813 我們可以一起探讨學習

注明:轉載可,需要附帶上文章連結