1. 程式人生 > 實用技巧 >包子學系列——Java基礎第八章_多執行緒

包子學系列——Java基礎第八章_多執行緒

第八章 多執行緒

程式、程序、執行緒

程式(programm)

概念:是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼。

程序(process)

概念:程式的一次執行過程,或是正在執行的一個程式。
說明:程序作為資源分配的單位,系統在執行時會為每個程序分配不同的記憶體區域

執行緒(thread)

概念:程序可進一步細化為執行緒,是一個程式內部的一條執行路徑。
說明:執行緒作為排程和執行的單位,每個執行緒擁獨立的執行棧和程式計數器(pc),執行緒切換的開銷小。

補充

  • 記憶體結構:

程序可以細化為多個執行緒。
每個執行緒,擁有自己獨立的:棧、程式計數器
多個執行緒,共享同一個程序中的結構:方法區、堆。

一個Java應用程式java.exe,其實至少有三個執行緒:main()主執行緒,gc() 垃圾回收執行緒,異常處理執行緒。當然如果發生異常,會影響主執行緒。

  • 並行與併發
  • 並行:多個CPU同時執行多個任務。比如:多個人同時做不同的事。
  • 併發:一個CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事。

建立多執行緒的兩種方式

方式一:繼承Thread類的方式

* 1. 建立一個繼承於Thread類的子類
* 2. 重寫Thread類的run() --> 將此執行緒執行的操作宣告在run()中
* 3. 建立Thread類的子類的物件
* 4. 通過此物件呼叫start():①啟動當前執行緒 ② 呼叫當前執行緒的run()

問題一:我們啟動一個執行緒,必須呼叫start(),不能呼叫run()的方式啟動執行緒。

問題二:如果再啟動一個執行緒,必須重新建立一個Thread子類的物件,呼叫此物件的start().

方式二:實現Runnable介面的方式

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

兩種方式的對比

* 開發中:優先選擇:實現Runnable介面的方式
* 原因:1. 實現的方式沒類的單繼承性的侷限性
*      2. 實現的方式更適合來處理多個執行緒共享資料的情況。
*
* 聯絡:public class Thread implements Runnable
* 相同點:兩種方式都需要重寫run(),將執行緒要執行的邏輯宣告在run()中。
          目前兩種方式,要想啟動執行緒,都是呼叫的Thread類中的start()。

JDK5.0 新增執行緒建立方式

  • 實現Callable介面。 --- JDK 5.0新增
  • 使用執行緒池
package com.atguigu.java2;

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

/*建立多執行緒的方式:
1.繼承Thread
2.實現Runnable
3.實現Callable
4.使用執行緒池

*/
class MyThread01 extends Thread {
    @Override
    public void run() {
        System.out.println("-----MyThread01");
    }
}

class MyThread02 implements Runnable {
    public void run() {
        System.out.println("-----MyThread02");
    }
}


//新方式
class MyThread03 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("-----MyThread03");
        return 200;
    }
}

//新方式
public class ThreadNew {
    public static void main(String[] args) {
        new MyThread01().start();
        new Thread(new MyThread02()).start();

        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread03());
        new Thread(futureTask).start();
        try {
            Integer value = futureTask.get();
            System.out.println(value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Java中多執行緒的建立有幾種方式?四種。

Thread類的常用方法

* 1. start():啟動當前執行緒;呼叫當前執行緒的run()
* 2. run(): 通常需要重寫Thread類中的此方法,將建立的執行緒要執行的操作宣告在此方法中
* 3. currentThread():靜態方法,返回執行當前程式碼的執行緒
* 4. getName():獲取當前執行緒的名字
* 5. setName():設定當前執行緒的名字
* 6. yield():釋放當前cpu的執行權
* 7. join():線上程a中呼叫執行緒b的join(),此時執行緒a就進入阻塞狀態,直到執行緒b完全執行完以後,執行緒a才結束阻塞狀態。
* 8. stop():已過時。當執行此方法時,強制結束當前執行緒。
* 9. sleep(long millitime):讓當前執行緒“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間內,當前執行緒是阻塞狀態。
* 10. isAlive():判斷當前執行緒是否存活

執行緒優先順序

* MAX_PRIORITY:10
* MIN _PRIORITY:1
* NORM_PRIORITY:5  -->預設優先順序


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

//getPriority():獲取執行緒的優先順序
//setPriority(int p):設定執行緒的優先順序

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

執行緒通訊:wait() / notify() / notifyAll() :此三個方法定義在Object類中的。

執行緒分類

一種是守護執行緒,一種是使用者執行緒。

區別是判斷JVM何時離開

  • 守護執行緒是用來服務使用者執行緒的,通過在start()方法前呼叫 thread.setDaemon(true)可以把一個使用者執行緒變成一個守護執行緒
  • Java垃圾回收就是一個典型的守護執行緒
  • 若JVM中都是守護執行緒,當前JVM將退出。

Thread的生命週期

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

執行緒的同步

問題的引出

例子:建立個視窗賣票,總票數為100張.使用實現Runnable介面的方式

問題:賣票過程中,出現了重票、錯票 -->出現了執行緒的安全問題

問題出現的原因:當某個執行緒操作車票的過程中,尚未操作完成時,其他執行緒參與進來,也操作車票。

如何解決:當一個執行緒a在操作ticket的時候,其他執行緒不能參與進來。直到執行緒a操作完ticket時,其他執行緒才可以開始操作ticket。這種情況即使執行緒a出現了阻塞,也不能被改變。

抽象化

  • 問題的原因:當多條語句在操作同一個執行緒共享資料時,一個執行緒對多條語句只執行了一部分,還沒有 執行完,另一個執行緒參與進來執行。導致共享資料的錯誤。
  • 解決辦法:對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以 參與執行。

Java解決方案:同步機制

Java對於多執行緒的安全問題提供了專業的解決方式:同步機制

方式一:同步程式碼塊

synchronized (物件){
    // 需要被同步的程式碼;
}

說明

  1. 操作共享資料的程式碼,即為需要被同步的程式碼。 -->不能包含程式碼多了,也不能包含程式碼少了。
  2. 共享資料:多個執行緒共同操作的變數。比如:ticket就是共享資料。
  3. 同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。要求:多個執行緒必須要共用同一把鎖。

補充

  1. 在實現Runnable介面建立多執行緒的方式中,我們可以考慮使用this充當同步監視器(鎖)。
  2. 在繼承Thread類建立多執行緒的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器。

方式二:同步方法

如果操作共享資料的程式碼完整的宣告在一個方法中,我們不妨將此方法宣告同步的。

public synchronized void show (String name){
    ….
}

關於同步方法的總結

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

方式三:Lock鎖 --- JDK5.0新增

synchronized 與 Lock的異同?

  • 相同:二者都可以解決執行緒安全問題

  • 不同:synchronized機制在執行完相應的同步程式碼以後,自動的釋放同步監視器

    Lock需要手動的啟動同步(lock(),同時結束同步也需要手動的實現(unlock())

Lock ---> 同步程式碼塊(已經進入了方法體,分配了相應資源 ) ---> 同步方法(在方法體之外)

利弊

同步的方式,解決了執行緒的安全問題。---好處

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

死鎖

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

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

public static void main(String[] args) {

    StringBuffer s1 = new StringBuffer();
    StringBuffer s2 = new StringBuffer();


    new Thread(){
        @Override
        public void run() {

            synchronized (s1){

                s1.append("a");
                s2.append("1");

                try {
                    Thread.sleep(100);
                } 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() {
        @Override
        public void run() {
            synchronized (s2){

                s1.append("c");
                s2.append("3");

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

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

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


            }



        }
    }).start();


}

加個sleep,讓死鎖的概率高一點

  • 第一個執行緒等待拿s2,第二個執行緒等待拿s1

執行緒通訊

執行緒通訊涉及到的三個方法

  • wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器。
  • notify():一旦執行此方法,就會喚醒被wait的一個執行緒。如果有多個執行緒被wait,就喚醒優先順序高的那個。
  • notifyAll():一旦執行此方法,就會喚醒所有被wait的執行緒。

說明

  • wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。
  • wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。否則,會出現IllegalMonitorStateException異常
  • wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中。

面試題

sleep() 和 wait()的異同?

  • 相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。
  • 不同點:
    • 兩個方法宣告的位置不同:Thread類中宣告sleep() , Object類中宣告wait()
    • 呼叫的要求不同:sleep()可以在任何需要的場景下呼叫。 wait()必須使用在同步程式碼塊或同步方法中
    • 關於是否釋放同步監視器:如果兩個方法都使用在同步程式碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

小結釋放鎖的操作

小結不會釋放鎖的操作