多線程的概述

多線程建立方式一:繼承Thread類

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

啓動:建立子類對象,調用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方法ide

啓動:建立實現類對象、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 關鍵字實現有序性

相關文章
相關標籤/搜索