多執行緒的概述

炒燜煎糖板栗發表於2021-03-04

多執行緒建立方式一:繼承Thread類

建立:繼承Thread類,重寫裡面的Run方法

啟動:建立子類物件,呼叫start方法

public class StartThread extends java.lang.Thread {
    @Override
    public void run() {
            for (int i = 0; i <10 ; i++) {
                System.out.println("一邊吃飯");
            }
    }
    public static void main(String[] args) {
        StartThread thread=new StartThread();
        thread.start();//開啟一個新的執行緒  下面的程式碼不受這句程式碼的影響不需要等待執行完成 繼續往下走
        for (int i = 0; i <10 ; i++) {
            System.out.println("一邊code");
        }
    }
}

執行結果

image-20210225230546729

首先進入Main方法,然後呼叫子類物件的Start方法,會啟動run方法,此時不需要等待run方法執行完畢,直接向繼續執行”一邊code“,start方法相當於開啟了一個新的執行緒,start方法但不保證立即執行。

如果把呼叫子類的方法改成run,就變成了普通方法,需要等待執行完成再進入下一步

public class StartThread extends java.lang.Thread {
    @Override
    public void run() {
            for (int i = 0; i <10 ; i++) {
                System.out.println("一邊吃飯");
            }
    }
    public static void main(String[] args) {
        StartThread thread=new StartThread();
        thread.run();//開啟一個新的執行緒  下面的程式碼不受這句程式碼的影響不需要等待執行完成 繼續往下走
        for (int i = 0; i <10 ; i++) {
            System.out.println("一邊code");
        }
    }
}

執行結果,先吃飯後code

image-20210225231319027

不建議使用,繼承了一個類,就不能繼承其他父類了

多執行緒建立方式二:實現Runnable介面

建立:實現Runnable介面 重寫Run方法

啟動:建立實現類物件、Thread物件 呼叫Start方法

public class RunnableThread   implements  Runnable{
    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            System.out.println("一邊吃飯");
        }
    }
    public static void main(String[] args) {
        RunnableThread runnableThread=new RunnableThread();
        Thread thread=new Thread(runnableThread);
        thread.start();
        for (int i = 0; i <20 ; i++) {
            System.out.println("一邊打遊戲");
        }
    }
}

image-20210228222818525

推薦:避免單繼承的侷限性,優先使用介面

實現Runnable介面模擬簡單搶票

建立三個使用者模仿搶票

public class RabbitClass extends RunnableThread {
    private  int num=99;
    @Override
    public void run() {
        while (true) {
            if(num<0)
            {
                break;
            }
            try {
                Thread.sleep(200);//模擬延時
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+num--);
        }
    }
    public static void main(String[] args) {
        RabbitClass rabbitClass=new RabbitClass();
        new Thread(rabbitClass,"one").start();//使用者一
        new Thread(rabbitClass,"two").start();//使用者二
        new Thread(rabbitClass,"three").start();//使用者三
    }
}

image-20210301225927620

多執行緒方式三:實現Callable介面

import java.util.concurrent.*;
public class Excallable implements Callable<Boolean> {
    private  int num=99;
    @Override
    public Boolean call() throws Exception {//模擬搶票
        while (true) {
            if(num<0)
            {
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"--->"+num--);
        }
        return  true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Excallable ccallable=new Excallable();
        //建立執行服務
        ExecutorService service= Executors.newFixedThreadPool(3);
        //提交執行
        Future<Boolean> retult=service.submit(ccallable);
        Future<Boolean> retult2=service.submit(ccallable);
        Future<Boolean> retult3=service.submit(ccallable);
        //獲取結果
        boolean r=retult.get();
        boolean r2=retult2.get();
        boolean r3=retult3.get();
        //關閉服務
        service.shutdownNow();
    }
}

執行緒的常用方法

1.1 Thread.currentThread()

Thread.currentThread()可以獲得當前執行緒,同一段程式碼可能被不同的執行緒執行,因此當前執行緒是相對的,Thread.currentThread()返回的是程式碼實際執行時候的執行緒物件。示例如下

public class SubThread extends Thread {
    public  SubThread()
    {
        System.out.println("main裡面呼叫的執行緒"+Thread.currentThread().getName());
    }
    @Override
    public void run() {
        System.out.println("run裡面呼叫的執行緒"+Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        System.out.println("main裡面呼叫的執行緒"+Thread.currentThread().getName());
        SubThread subThread=new SubThread();
        subThread.start();//子執行緒
    }
}

image-20210303214321586

在main方法裡面。呼叫執行緒所以是main執行緒,main裡面呼叫構造方法,所以構造方法也是呼叫main執行緒,當啟動子執行緒相當於開啟了一個新的執行緒。

1.2 Thread.setName()/getName()

setName可以設定執行緒名稱,getName可以獲取執行緒名稱,通過設定執行緒名稱有助於程式除錯,提高可讀性,建議為每一個執行緒設定一個可以體現執行緒功能的名稱。

1.3 isAlive()

isAlive可以判斷執行緒是否處於活動狀態,


public class SubThread extends Thread {
    @Override
    public void run() {
        System.out.println("run方法-->"+isAlive());
    }
    public static void main(String[] args) {java
        SubThread subThread=new SubThread();
        System.out.println("begin-->"+subThread.isAlive());
        subThread.start();
        System.out.println("end-->"+subThread.isAlive());//此時執行緒結束有可能返回false,不定性
    }
}

image-20210303204701082

1.4 Sleep()

sleep方法讓當前執行緒休眠指定毫秒數

1.5 getId()

Thread.getId()可以獲得執行緒的唯一標識

某個編號的執行緒執行結束之後可能又被其他執行緒使用,重啟JVM之後,同一個執行緒的id可能不一樣。

1.6 yieId()

Thread.yieId()方法作用是放棄當前的CPU資源

1.7 setPripority()

Thread.setpropority(num)設定執行緒優先順序

java執行緒優先順序取值範圍:1~10,超過這個範圍會異常

作業系統中,優先順序較高的執行緒獲得CPU的資源比較多

執行緒的優先順序本質上是給執行緒排程器一個提示,用於決定先排程那些執行緒,並不能保證執行緒先執行

優先順序如果設定不當可能導致某些執行緒永遠無法執行,即產生了執行緒飢餓。

執行緒優先順序並不是設定的越高越好,一般設定普通優先順序就好。執行緒的優先順序具有繼承性,在A執行緒中建立B執行緒,則B執行緒的優先順序與A執行緒一樣。

1.8 interrupt()

中斷執行緒,該方法僅僅是在當前執行緒打一個停止標誌,並不是真正的停止執行緒

public class SubThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <10000 ; i++) {
            System.out.println("run-->"+i);
            if(this.isInterrupted())
            {
                System.out.println("執行緒中斷退出");
                return;//直接結束run方法
            }
        }
    }
    public static void main(String[] args) {
        SubThread subThread=new SubThread();
        subThread.start();//子執行緒
        for (int i = 0; i <100 ; i++) {
            System.out.println("main-->"+i);
        }
        subThread.interrupt();//標記執行緒中斷此時isInterrupted=true 執行緒並沒有中斷
    }
}

image-20210303214907554

1.9 setDaemon()

java 中執行緒分為使用者執行緒和守護執行緒

守護執行緒是為其他執行緒提供服務的執行緒,如垃圾回收器(GC)就是一個守護執行緒

守護執行緒不能單獨執行,當JVM中沒有其他使用者執行緒,只有守護執行緒,守護執行緒會自動銷燬。

public class SubThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <10000 ; i++) {
            System.out.println("run-->"+i);
        }
    }
    public static void main(String[] args) {
        SubThread subThread=new SubThread();
        subThread.setDaemon(true);//設定執行緒守護
        subThread.start();//子執行緒
        for (int i = 0; i <100 ; i++) {
            System.out.println("main-->"+i);
        }
    }
}

image-20210303220033859

設定執行緒守護以後,子執行緒run執行了一段才停止,因為設定執行緒守護以後銷燬需要時間。

執行緒的生命週期

執行緒的生命週期可以通過getstate()獲得,Thread.state型別分為

New:新建狀態,建立了執行緒物件,在Start啟動前的狀態

Runnable可執行狀態:包含READY,表示該執行緒可以被資源排程器進行排程。使它處於RUNNING狀態,RUNNING狀態表示該執行緒正在執行,如果用yieid方法可以把RUNNING狀態轉化為READY狀態

Waiting等待狀態:執行緒執行了wait()、thread.join 方法會把執行緒轉化為Waiting等待狀態,執行object.notify()方法,或者加入的執行緒執行完畢,當前執行緒會轉化為RUNNABLE狀態。

TimeD_WAITING狀態:跟Waiting類似,但是如果沒有在指定範圍實際完成期望操作,會自動轉化為RUNNABLE狀態。

TERMINARED狀態:,終止,執行緒結束

多執行緒狀態圖

image-20210304204149223

多執行緒的優勢和缺點

優勢

  1. 提高系統的吞吐率,多執行緒可以使一個程式有多個併發的操作
  2. 提高響應性,WEB伺服器會採用一些專門的執行緒負責處理請求操作,縮短使用者等待時間
  3. 充分利用多核處理器資源,通過多執行緒可以充分的利用CPU資源避免浪費

劣勢

執行緒安全問題,多執行緒共享資料時,如果沒有采取正確的併發控制措施,就可能產生資料一致性的問題,如讀取過期的資料,丟失資料更新。

執行緒活性問題,由於程式自身的缺陷導致哦執行緒一直處於非RUNNABLE狀態,常見的活性故障有:

  • 死鎖(DEADLOOK):類似與鷸蚌相爭
  • 鎖死(LockOut):類似於睡美人故事的王子掛了,一直處於一種狀態沒有喚醒
  • 活鎖(Livelock):類似於小貓咬自己尾巴
  • 飢餓(Starvation):類似於健壯的雛鳥總是聰母鳥的嘴巴里搶到食物

上下文切換(Context Switch)問題,處理器從一個執行緒切換到另一個執行緒

可靠性問題,可能會由一個執行緒導致JVM意外終止,其他執行緒無法執行

多執行緒的執行緒安全問題

非執行緒安全就是指多個執行緒對同一個例項物件進行操作的時候有隻被更改或者值不同步的問題。

執行緒安全問題表現在三個方面:

1.原子性

原子就是不可分割的意思,有兩層含義:

(1)訪問共享變數的操作,其他執行緒來看,要麼已經關閉,要麼執行完成,其他執行緒看不到這個操作的中間結果

(2)訪問同一種共享變數的原子操作是不能交錯的

使用者ATM取錢,要麼成功取到錢了餘額發生變更,要麼失敗什麼都沒有變

java有兩種方法實現原子性:

(1)使用鎖(鎖具有排它性,一時刻只能被一個執行緒訪問)

(2)使用處理器的CAS指令(硬體鎖)

2.可見性

在多執行緒中,一個執行緒對某個共享變數進行修改,其他執行緒可能不能立即獲取到這個更新的結果

如果更新之後能獲取到則這個執行緒具有可見性,否則不具有可見性。可能會導致其他執行緒讀取到髒資料。

3.有序性

有序性是指在某些情況,下一個處理器上執行的一個執行緒所執行的記憶體訪問操作在另一個處理器的其他執行緒看來是亂序的。

在多核處理器的環境下,編寫程式碼的順序可能不會是執行的順序,在一個處理器上執行的順序,在其他處理器上看起來和程式碼不一樣,這種現象稱為重排序。重排序是對記憶體訪問操作的優化,前提是單執行緒,但是對多執行緒的正確效能可能會有影響。

操作順序概念

  • 原始碼順序,指原始碼中指定的記憶體訪問順序
  • 程式順序,處理器上執行的目的碼所指向的記憶體訪問順序
  • 感知順序,給定處理器所感知到的該處理器以及其他處理器記憶體訪問的操作順序
  • 執行順序,記憶體訪問操作在處理器上的執行順序

可以把重排序分為指令重排序和儲存子系統重排序:

指令重排序主要有JIT編譯器處理器引起的,指程式順序和執行順序不一樣

指令重排序是一種動作,確實對指令的順序做了調整,Javac編譯器一般不會執行指令重排序,而JIT編譯器可能執行。CPU處理器可能執行指令重排序,使得執行順序與程式順序不一致。

儲存子系統重排序是由快取記憶體,寫緩衝器引起的,感知順序和執行順序不一致。

快取記憶體是cpu為了匹配與主記憶體處理速度不匹配而設計的快取記憶體,寫快取器用來提高寫快取記憶體的效率,即使處理器嚴格執行兩個記憶體的訪問操作,在儲存子系統的作用下其他處理器對操作的操作順序和感知順序可能不一致。

儲存子系統排序並沒有對指令順序進行排序,而是造成指令執行順序被調整的假象。儲存子系統重排序物件是記憶體操作的結果。

從處理器角度來看, 讀記憶體就是從指定的 RAM 地址中載入資料到 暫存器,稱為 Load 操作; 寫記憶體就是把資料儲存到指定的地址表示 的 RAM 儲存單元中,稱為 Store 操作.

記憶體重排序有以下四種可能:

  • LoadLoad 重排序,一個處理器先後執行兩個讀操作 L1 和 L2,其他處 理器對兩個記憶體操作的感知順序可能是 L2->L1
  • toreStore重排序一個處理器先後執行兩個寫操作W1和W2,其他 處理器對兩個記憶體操作的感知順序可能是 W2->W1
  • LoadStore 重排序,一個處理器先執行讀記憶體操作 L1 再執行寫記憶體 操作 W1, 其他處理器對兩個記憶體操作的感知順序可能是 W1->L1
  • StoreLoad重排序,一個處理器先執行寫記憶體操作W1再執行讀記憶體 操作 L1, 其他處理器對兩個記憶體操作的感知順序可能是 L1->W1

記憶體重排序與具體的處理器微架構有關,不同架構的處理器所允許的記憶體重序不同

貌似序列語義

JIT 編譯器,處理器,儲存子系統是按照一定的規則對指令,記憶體操作的結果進行重排序, 給單執行緒程式造成一種假象----指令是按照原始碼 的順序執行的.這種假象稱為貌似序列語義. 並不能保證多執行緒環境 程式的正確性

為了保證貌似序列語義,有資料依賴關係的語句不會被重排序,只 有不存在資料依賴關係的語句才會被重排序.如果兩個操作(指令)訪 問同一個變數,且其中一個操作(指令)為寫操作,那麼這兩個操作之間 就存在資料依賴關係(Data dependency).

x = 1; y = x + 1; 後一條語句的運算元包含前一條語句的執行結果

如果不存在資料依賴關係則可能重排序,如:

double price = 45.8;  int quantity = 10; double sum = price * quantity;

保證記憶體訪問的順序性

可以使用 volatile 關鍵字, synchronized 關鍵字實現有序性

相關文章