多線程的概述

多線程創建方式一:繼承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 關鍵字實現有序性

Tags: