JavaSE第18篇:多線程上篇

核心內(nèi)容:在實際開發(fā)中,若程序需要同時處理多個任務(wù)時,我們該如何實現(xiàn)?此時多線程就可幫助我們實現(xiàn)。使用多線程可以提高CPU的利用率及程序的處理效率。本篇將會學(xué)習(xí)多線程相關(guān)概念、創(chuàng)建和使用、線程安全問題及線程狀態(tài)的了解。

第一章:多線程基礎(chǔ)

本章主要了解和多線程相關(guān)的一些概念。

想要設(shè)計一個程序,邊打游戲邊聽歌,怎么設(shè)計?

要解決上述問題,得使用多進(jìn)程或者多線程來解決.

1.1-并發(fā)和并行(了解)

并發(fā)

并發(fā)簡而言之就是:指兩個或多個事件在同一個時間段內(nèi)發(fā)生 (交替執(zhí)行)。

在操作系統(tǒng)中,安裝了多個程序,并發(fā)指的是在一段時間內(nèi)宏觀上有多個程序同時運行,這在單 CPU 系統(tǒng)中,每 一時刻只能有一道程序執(zhí)行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。

image

并行

簡而言之,并行:是指兩個或多個事件在同一時刻發(fā)生(同時發(fā)生)。

而在多個 CPU 系統(tǒng)中,則這些可以并發(fā)執(zhí)行的程序便可以分配到多個處理器上(CPU),實現(xiàn)多任務(wù)并行執(zhí)行,即利用每個處理器來處理一個可以并發(fā)執(zhí)行的程序,這樣多個程序便可以同時執(zhí)行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,并行處理的程序越多,能大大的提高電腦運行的效率。

image

1.2-進(jìn)程與線程 (了解)

進(jìn)程

是指一個內(nèi)存中運行的應(yīng)用程序,每個進(jìn)程都有一個獨立的內(nèi)存空間,一個應(yīng)用程序可以同時運行多個進(jìn)程;進(jìn)程也是程序的一次執(zhí)行過程,是系統(tǒng)運行程序的基本單位;系統(tǒng)運行一個程序即是一個進(jìn)程從創(chuàng)建、運行到消亡的過程。

image

線程

線程是進(jìn)程中的一個執(zhí)行單元,負(fù)責(zé)當(dāng)前進(jìn)程中程序的執(zhí)行,一個進(jìn)程中至少有一個線程。一個進(jìn)程

中是可以有多個線程的,這個應(yīng)用程序也可以稱之為多線程程序。

image

注意

一個程序運行后至少有一個進(jìn)程,一個進(jìn)程中可以包含多個線程。

由于創(chuàng)建一個線程的開銷比創(chuàng)建一個進(jìn)程的開銷小的多,那么我們在開發(fā)多任務(wù)運行的時候,通??紤]創(chuàng)建多線程,而不是創(chuàng)建多進(jìn)程。

多線程可以提高cpu利用率

大部分操作系統(tǒng)都支持多進(jìn)程并發(fā)運行,現(xiàn)在的操作系統(tǒng)幾乎都支持同時運行多個程序。在同時運行的程序,”感覺這些軟件好像在同一時刻運行著“。

實際上,CPU(中央處理器)使用搶占式調(diào)度模式在多個線程間進(jìn)行著高速的切換。對于CPU的一個核而言,某個時刻,只能執(zhí)行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。 其實,多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

1.3-線程調(diào)度(了解)

分時調(diào)度

所有線程輪流使用 CPU 的使用權(quán),平均分配每個線程占用 CPU 的時間。

搶占式調(diào)度

優(yōu)先讓優(yōu)先級高的線程使用 CPU,如果線程的優(yōu)先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調(diào)度。

image

第二章:Java中創(chuàng)建和使用多線程

2.1-繼承Thread類方式創(chuàng)建線程(重要)

Java使用 java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是 完成一定的任務(wù),實際上就是執(zhí)行一段程序流即一段順序執(zhí)行的代碼。Java使用線程執(zhí)行體來代表這段程流。

Java中通過繼承Thread類來創(chuàng)建并啟動多線程的步驟如下

  1. 定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務(wù),因此把 run()方法稱為線程執(zhí)行體。
  2. 創(chuàng)建Thread子類的實例,即創(chuàng)建了線程對象 。
  3. 調(diào)用線程對象的start()方法來啟動該線程 。

Thread類構(gòu)造方法

  • public Thread() :分配一個新的線程對象。
  • public Thread(String name) :分配一個指定名字的新的線程對象。

示例代碼

/*測試類中的代碼*/
public class DemoThread {
  public static void main(String[] args) {
    MyThread mt = new MyThread("線程1");
    mt.start(); // 啟動線程1的任務(wù)
    MyThread mt2 = new MyThread("線程2");
    mt2.start(); // 啟動線程2的任務(wù)

  }
}
/*定義的線程類代碼*/
public class MyThread extends Thread {
  public MyThread(String name) {
    super(name);
  }

  @Override
  public void run() {
    for (int i = 0; i < 20; i++) {
      System.out.println(getName() + "線程執(zhí)行" + i);
    }
  }
}

2.2-多線程原理(了解)

多個線程之間的程序不會影響彼此(比如一個線程崩潰了并不會影響另一個線程)。

在Java中,main方法是程序執(zhí)行的入口,也是Java程序的主線程。當(dāng)在程序中開辟新的線程時,執(zhí)行過程是這樣的。

執(zhí)行過程

  1. 首先main方法作為主程序先壓棧執(zhí)行。

  2. 在主程序的執(zhí)行過程中,若創(chuàng)建了新的線程,則內(nèi)存中會另開辟一個新的棧來執(zhí)行新的線程。

  3. 每一個新的線程都會有一個新的棧來存放新的線程任務(wù)。

  4. 棧與棧之間的任務(wù)不會互相影響。

  5. CPU會隨機切換執(zhí)行不同棧中的任務(wù)。

圖解執(zhí)行過程(以上述代碼為例)

image

2.3-Thread類常用方法(重要)

常用方法

  • public String getName() :獲取當(dāng)前線程名稱。
  • public void start() :導(dǎo)致此線程開始執(zhí)行; Java虛擬機調(diào)用此線程的run方法。
  • public void run() :此線程要執(zhí)行的任務(wù)在此處定義代碼。
  • public static void sleep(long millis) :使當(dāng)前正在執(zhí)行的線程以指定的毫秒數(shù)暫停(暫時停止執(zhí)行)。
  • public static Thread currentThread() :返回對當(dāng)前正在執(zhí)行的線程對象的引用。

示例代碼

//【代碼測試類】
public class Main01 {
  public static void main(String[] args) {
    MyThread mt = new MyThread("線程1");
    mt.start();
    // 打印線程名稱
    System.out.println(mt.getName());
    System.out.println("當(dāng)前線程是" + Thread.currentThread().getName());
    // 每間隔一秒鐘打印一個數(shù)字
    for (int i = 0; i < 60; i++) {
      System.out.println(i);
      try {
        // sleep拋出了異常,需要處理異常
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}
//【MyThread類】
public class MyThread extends Thread{
  public  MyThread(){
    super();
  }
  // 構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)傳入線程名稱
  public MyThread(String name) {
    super(name);
  }
  @Override
  public void run() {
    // 打印線程名稱
    System.out.println(this.getName());
    System.out.println("當(dāng)前線程是" + Thread.currentThread().getName());
  }
}

run方法和start方法

run()方法,是線程執(zhí)行的任務(wù)方法,每個線程都會調(diào)用run()方法執(zhí)行,我們將線程要執(zhí)行的任務(wù)代碼都寫在run()方法中就可以被線程調(diào)用執(zhí)行。

start()方法,開啟線程,線程調(diào)用run()方法。start()方法源代碼中會調(diào)用本地方法start0()來啟動線程:private native void start0(),本地方法都是和操作系統(tǒng)交互的,因此可以看出每次開啟一個線程的線程都會和操作系統(tǒng)進(jìn)行交互。

注意:一個線程只能被啟動一次!

關(guān)于線程的名字

線程是有默認(rèn)名字的,如果我們不設(shè)置線程的名字,JVM會賦予線程默認(rèn)名字Thread-0,Thread-1。

2.4-實現(xiàn)Runnable接口方式創(chuàng)建線程(重要)

翻閱API后得知創(chuàng)建線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現(xiàn)Runnable接口方式 。

Runnable使用步驟

  1. 定義Runnable接口的實現(xiàn)類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執(zhí)行體。
  2. 創(chuàng)建Runnable實現(xiàn)類的實例,并以此實例作為Thread的target來創(chuàng)建Thread對象,該Thread對象才是真正的線程對象。
  3. 調(diào)用線程對象的start()方法來啟動線程。

Thread類構(gòu)造函數(shù)

  • public Thread(Runnable target) :分配一個帶有指定目標(biāo)新的線程對象。
  • public Thread(Runnable target,String name) :分配一個帶有指定目標(biāo)新的線程對象并指定名字。

示例代碼

// 測試類
public class Main01 {
  public static void main(String[] args) {
    // 創(chuàng)建Runnable對象
    RunnableImpl ra = new RunnableImpl();
    // 創(chuàng)建線程對象并傳入Runnable對象
    Thread th = new Thread(ra);
    // 啟動并執(zhí)行線程任務(wù)
    th.start();
  }
}
// 【Runnable實現(xiàn)類】
public class RunnableImpl implements Runnable {
  @Override
  public void run() {
    System.out.println("線程任務(wù)1");
  }
}

總結(jié)

  • 通過實現(xiàn)Runnable接口,使得該類有了多線程類的特征。run()方法是多線程程序的一個執(zhí)行目標(biāo)。所有的多線程代碼都在run方法里面。Thread類實際上也是實現(xiàn)了Runnable接口的類。
  • 在啟動的多線程的時候,需要先通過Thread類的構(gòu)造方法Thread(Runnable target) 構(gòu)造出對象,然后調(diào)用Thread對象的start()方法來運行多線程代碼。
  • 實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現(xiàn)Runnable接口來實現(xiàn)多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進(jìn)行多線程編程的基礎(chǔ)。
  • Runnable對象僅僅作為Thread對象的target,Runnable實現(xiàn)類里包含的run()方法僅作為線程執(zhí)行體。而實際的線程對象依然是Thread實例,只是該Thread線程負(fù)責(zé)執(zhí)行其target的run()方法。

2.5-Runnable和Thread的關(guān)系(了解)

創(chuàng)建線程方式2好像比創(chuàng)建線程方式1操作要麻煩一些,為何要多此一舉呢?

因為如果一個類繼承Thread,則不適合資源共享。但是如果實現(xiàn)了Runable接口的話,則很容易的實現(xiàn)資源共享。

實現(xiàn)Runnable接口比繼承Thread類所具有的優(yōu)勢:

  • 適合多個相同的程序代碼的線程去共享同一個資源。
  • 可以避免java中的單繼承的局限性。
  • 增加程序的健壯性,實現(xiàn)解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
  • 線程池只能放入實現(xiàn)Runable或Callable類線程,不能直接放入繼承Thread的類。(后面篇幅介紹)

擴(kuò)展了解

在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當(dāng)使用java命令執(zhí)行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在操作系統(tǒng)中啟動了一個進(jìn)程。

2.6-匿名內(nèi)部類方式實現(xiàn)線程創(chuàng)建(重要)

使用線程的內(nèi)匿名內(nèi)部類方式,可以方便的實現(xiàn)每個線程執(zhí)行不同的線程任務(wù)操作。

簡而言之,使用匿名內(nèi)部類可以簡化代碼。

    // 匿名內(nèi)部類創(chuàng)建線程方式1
    new Thread(){
      @Override
      public void run() {
        System.out.println(Thread.currentThread().getName());
      }
    }.start();
    // 匿名內(nèi)部類創(chuàng)建線程方式2
    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println(Thread.currentThread().getName());
      }
    }).start();

第三章:線程安全問題

3.1-線程安全概述(理解)

多個線程執(zhí)行同一個任務(wù)并操作同一個數(shù)據(jù)時,就會造成數(shù)據(jù)的安全問題。我們通過以下案例來看線程安全問題。

案例需求

電影院要賣票,我們模擬電影院的賣票過程。假設(shè)要播放的電影是 “皮卡丘大戰(zhàn)葫蘆娃”,本次電影的座位共100個 (本場電影只能賣100張票)。

我們來模擬電影院的售票窗口,實現(xiàn)多個窗口同時賣 “皮卡丘大戰(zhàn)葫蘆娃”這場電影票(多個窗口一起賣這100張票) 需要窗口,采用線程對象來模擬;需要票,Runnable接口子類來模擬 。

案例代碼

//【操作票的任務(wù)代碼類】
public class RunnableImpl implements Runnable {
  // 線程任務(wù)要操作的數(shù)據(jù)(100張電影票)
  private int ticket = 100;
  // 線程要執(zhí)行的任務(wù)
  @Override
  public void run() {
    while (true){
        if(ticket>0){
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket+"張票");
            ticket--;
        }else {
            break;
        }
    }
  }
}
//【測試類】
public class Main01 {
  public static void main(String[] args) {
    // 創(chuàng)建線程任務(wù)
    RunnableImpl ra = new RunnableImpl();
    // 創(chuàng)建第一個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第二線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第三個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();

  }
}

執(zhí)行結(jié)果及問題

image

問題原因

搶奪cpu執(zhí)行權(quán)和線程執(zhí)行時間是不確定的,比如線程0搶到了cpu執(zhí)行權(quán)并執(zhí)行到了打印代碼處,此時cpu又被線程1搶奪,其他線程處于等待線程1頁執(zhí)行到了打印代碼處,沒等ticket--,兩個線程都打印了售票信息。

這種問題,幾個窗口(線程)票數(shù)不同步了,這種問題稱為線程不安全。

線程安全問題都是由全局變量及靜態(tài)變量引起的。若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫 操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步, 否則的話就可能影響線程安全。

3.2-線程安全解決方案(重要)

上述我們知道,線程安全問題是因為線程在操作數(shù)據(jù)時不同步造成的,所以只要能夠?qū)崿F(xiàn)操作數(shù)據(jù)同步,就可以解決線程安全問題。

同步指的就是,當(dāng)一個線程執(zhí)行指定同步的代碼任務(wù)時,其他線程必須等該線程操作完畢后再執(zhí)行。

根據(jù)案例描述:窗口1線程進(jìn)入操作的時候,窗口2和窗口3線程只能在外等著,窗口1操作結(jié)束,窗口1和窗口2和窗口3才有機會進(jìn)入代碼 去執(zhí)行。也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待修改完畢同步之后,才能去搶奪CPU 資源,完成對應(yīng)的操作,保證了數(shù)據(jù)的同步性,解決了線程不安全的現(xiàn)象。

為了保證每個線程都能正常執(zhí)行原子操作,Java引入了線程同步機制(synchronize)。

那么怎么去使用呢?有三種方式完成同步操作:

  1. 同步代碼塊
  2. 同步方法
  3. 同步鎖

3.3-同步代碼塊(重要)

概述

同步代碼塊: synchronized關(guān)鍵字可以用于方法中的某個區(qū)塊中,表示只對這個區(qū)塊的資源實行互斥訪問。

格式

synchronized(同步鎖){ 需要同步操作的代碼 }

  • 同步鎖:對象的同步鎖只是一個概念,可以想象為在對象上標(biāo)記了一個鎖。
  • 鎖對象可以是任意類型。
  • 多個線程對象 要使用同一把鎖。
  • 注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進(jìn)入代碼塊,其他的線程只能在外等著 。

示例代碼

//【測試類】
public class Main01 {
  public static void main(String[] args) {
    // 創(chuàng)建線程任務(wù)
    RunnableImpl ra = new RunnableImpl();
    // 創(chuàng)建第一個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第二線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第三個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();

  }
}
//【線程任務(wù)類】
public class RunnableImpl implements Runnable {
  // 線程任務(wù)要操作的數(shù)據(jù)
  private int ticket = 100;
  // 定義線程鎖對象(任意對象)
  Object obj = new Object();
  // 線程任務(wù)
  @Override
  public void run() {
    while (true){
      synchronized (obj){
        if(ticket>0){
          try {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"在售賣第" + ticket + "張票");
            ticket--;
          } catch (InterruptedException e) {
            e.printStackTrace();
          }

        }else{
            break;
        }
      }

    }
  }
}

3.4-同步方法(重要)

概述

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執(zhí)行該方法的時候,其他線程只能在方法外等著

格式

public synchronized void method(){ // 可能會產(chǎn)生線程安全問題的代碼 }

同步鎖是誰?

  • 對于非static方法,同步鎖就是this
  • 對于static方法,我們使用當(dāng)前方法所在類的字節(jié)碼對象(類名.class)。

示例代碼

//【測試類】
public class Main01 {
  public static void main(String[] args) {
    // 創(chuàng)建線程任務(wù)
    RunnableImpl ra = new RunnableImpl();
    // 創(chuàng)建第一個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第二線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第三個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
  }
}
//【線程任務(wù)類】
public class RunnableImpl implements Runnable {
  // 線程任務(wù)要操作的數(shù)據(jù)
  private int ticket = 100;
  @Override
  public void run() {
    while (true) {
      int flag = func();
      if(flag==0) {
          break;
      }
    }

  }
  public synchronized int func() {
    if (ticket > 0) {
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
      ticket--;
      return 1;
    }else {
        return 0;
    }
  }
}

3.5-同步鎖(重要)

概述

Lock:java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作, 同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現(xiàn)面向?qū)ο蟆?/p>

格式

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了

  • public void lock() :加同步鎖。
  • public void unlock() :釋放同步鎖。

示例代碼

//【測試類】
public class Main01 {
  public static void main(String[] args) {
    // 創(chuàng)建線程任務(wù)
    RunnableImpl ra = new RunnableImpl();
    // 創(chuàng)建第一個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第二線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
    // 創(chuàng)建第三個線程執(zhí)行線程任務(wù)
    new Thread(ra).start();
  }
}
//【線程任務(wù)類】
public class RunnableImpl implements Runnable {
  // 線程任務(wù)要操作的數(shù)據(jù)
  private int ticket = 100;
  // 創(chuàng)建鎖對象
  Lock lock = new ReentrantLock();
  @Override
  public void run() {
    while (true) {
      // 開啟同步鎖
      lock.lock();
      if (ticket > 0) {
        try {
          Thread.sleep(10);
          System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
          ticket--;
        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally {
          // 釋放同步鎖
          lock.unlock();
        }

      }else {
          break;
      }
    }

  }
}
 

第四章:線程狀態(tài)

image

4.1-線程狀態(tài)介紹(了解)

當(dāng)線程被創(chuàng)建并啟動以后,它既不是一啟動就進(jìn)入了執(zhí)行狀態(tài),也不是一直處于執(zhí)行狀態(tài)。在線程的生命周期中, 有幾種狀態(tài)呢?在API中 java.lang.Thread.State 這個枚舉中給出了六種線程狀態(tài):

image

我們不需要去研究這幾種狀態(tài)的實現(xiàn)原理,我們只需知道在做線程操作中存在這樣的狀態(tài)。那我們怎么去理解這幾 個狀態(tài)呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態(tài)與非運行狀態(tài)之間 的轉(zhuǎn)換問題。

4.2-TimedWaiting計時等待(了解)

概述

Timed Waiting在API中的描述為:一個正在限時等待另一個線程執(zhí)行一個(喚醒)動作的線程處于這一狀態(tài)。

單獨 的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經(jīng)接觸過這個狀態(tài)了,在哪里呢? 在我們寫賣票的案例中,為了減少線程執(zhí)行太快,現(xiàn)象不明顯等問題,我們在run方法中添加了sleep語句,這樣就 強制當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行),以“減慢線程”。

其實當(dāng)我們調(diào)用了sleep方法之后,當(dāng)前執(zhí)行的線程就進(jìn)入到“休眠狀態(tài)”,其實就是所謂的Timed Waiting(計時等 待),那么我們通過一個案例加深對該狀態(tài)的一個理解。

示例

需求:實現(xiàn)一個計數(shù)器,計數(shù)到100,在每個數(shù)字之間暫停1秒,每隔10個數(shù)字輸出一個字符串 。

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print(" 線程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
    }
}

通過案例可以發(fā)現(xiàn),sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進(jìn)入 TIMED_WAITING 狀態(tài)的一種常見情形是調(diào)用的 sleep 方法,單獨的線程也可以調(diào)用,不一定非要有協(xié) 作關(guān)系。
  2. 為了讓其他線程有機會執(zhí)行,可以將Thread.sleep()的調(diào)用放線程run()之內(nèi)。這樣才能保證該線程執(zhí)行過程 中會睡眠 。
  3. sleep與鎖無關(guān),線程睡眠到期自動蘇醒,并返回到Runnable(可運行)狀態(tài) 。

注意:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期后就 開始立刻執(zhí)行。

圖解

image

4.3-Blocked鎖阻塞(了解)

概述

Blocked狀態(tài)在API中的介紹為:一個正在阻塞等待一個監(jiān)視器鎖(鎖對象)的線程處于這一狀態(tài) 。

我們已經(jīng)學(xué)完同步機制,那么這個狀態(tài)是非常好理解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲 取到鎖,線程A進(jìn)入到Runnable狀態(tài),那么線程B就進(jìn)入到Blocked鎖阻塞狀態(tài)。

這是由Runnable狀態(tài)進(jìn)入Blocked狀態(tài)。除此Waiting以及Time Waiting狀態(tài)也會在某種情況下進(jìn)入阻塞狀態(tài)。

圖解

image

4.4-Waiting 無限等待(了解)

概述

Wating狀態(tài)在API中介紹為:一個正在無限期等待另一個線程執(zhí)行一個特別的(喚醒)動作的線程處于這一狀態(tài)。

示例

我們通過一段代碼來 學(xué)習(xí)一下:需求如下,消費者吃包子。過程如下:

  • 消費者問:包子好了嗎? 處于等待...
  • 3秒鐘后....
  • 老板答:包子好了
  • 消費者:可以吃包子了

示例代碼:

public class Test04 {
    // 鎖對象
    public static Object obj = new Object();

    public static void main(String[] args) {
        // 【消費者線程】
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (obj) {
                        System.out.println(Thread.currentThread().getName() + "-顧客1:老板包子好了嗎?");
                        try {
                            // 等待,釋放鎖,處于阻塞狀態(tài)
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 喚醒之后要執(zhí)行的代碼
                        System.out.println(Thread.currentThread().getName() + "顧客1:可以吃包子了。");
                        System.out.println("--------------------------------------");
                    }
                }
            }
        }).start();
        // 【生產(chǎn)者線程】
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        System.out.println("等待3秒后...");
                        System.out.println(Thread.currentThread().getName() + "老板說:包子好了!");
                        // 喚醒,喚醒其他被阻塞的線程
                        obj.notify();
                    }
                }
            }
        }).start();
    }
}

分析

通過上述案例我們會發(fā)現(xiàn),一個調(diào)用了某個對象的 Object.wait 方法的線程會等待另一個線程調(diào)用此對象的 Object.notify()方法 或 Object.notifyAll()方法 。

其實waiting狀態(tài)并不是一個線程的操作,它體現(xiàn)的是多個線程間的通信,可以理解為多個線程之間的協(xié)作關(guān)系, 多個線程會爭取鎖,同時相互之間又存在協(xié)作關(guān)系。就好比在公司里你和你的同事們,你們可能存在晉升時的競 爭,但更多時候你們更多是一起合作以完成某些任務(wù)。

當(dāng)多個線程協(xié)作時,比如A,B線程,如果A線程在Runnable(可運行)狀態(tài)中調(diào)用了wait()方法那么A線程就進(jìn)入 了Waiting(無限等待)狀態(tài),同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態(tài)中調(diào)用了 notify()方法,那么就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那么A線程喚醒后就進(jìn)入 Runnable(可運行)狀態(tài);如果沒有獲取鎖對象,那么就進(jìn)入到Blocked(鎖阻塞狀態(tài))。

圖解

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容