Java并發之線程

?????在前面我們介紹的一些內容中,我們的程序都是一條執行流,一步一步的執行。但其實這種程序對我們計算機的資源的使用上是低效的。例如:我們有一個用于計算的程序,主程序計算數據,在計算的過程中每得到一個結果就需要將其保存到外部磁盤上,那么難道我們的主程序每次都要停止等待CPU將結果保存到磁盤之后,再繼續完成計算工作嗎?要知道磁盤的速度可是巨慢的(相對內存而言),我們如果能分一個線程去完成磁盤的寫入工作,主線程還是繼續計算的話,是不是效率更高了呢?其實,并發就是這樣的一種思想,使用時間片分發給各個線程CPU的使用時間,給人感覺好像程序在同時做多個事情一樣,這樣做的好處主要在于它能夠對我們整個的計算機資源有一個充分的利用,在多個線程競爭計算機資源不沖突的前提下,充分的利用我們的資源。本篇文章首先來介紹并發的最基本的內容-----線程。主要涉及以下一些內容:

  • 定義線程的兩種不同的方法及它們之間的區別
  • 線程的幾種不同的狀態及其區別
  • Thread類中的一些線程屬性和方法
  • 多線程遇到的幾個典型的問題

?????一、創建一個線程
?????首先我們看創建一個線程的第一種方式,繼承Thread類并重寫其run方法。

public class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println("this is mythread");
    }
}

現在我們來看看在主程序中如何啟動我們自定義的線程:

public static void main(String[] args) {
    Thread myThread = new MyThread();
    myThread.start();
}

我們首先構建一個Thread實例,調用其start方法,調用該方法會為線程分配其所必須的堆棧資源,計數器,時間片等,并在該方法的結束時刻調用我們重寫的run方法,完成線程的啟動。

但是在Java中類是單繼承的,也就是如果某個類已經有了父類,那么它就不能被定義成線程類。當然,Java中也提供了第二種方法來定義一個線程類,這種方式實際上更加的接近本質一些。通過繼承接口Runnable并在其內部重寫一個run方法。

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("this is mythread");
    }
}

啟動線程的方式和上一種略微有點不同,但是本質上都是一樣的。

public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.start();
    }

這里我們利用Thread的一個構造函數,傳入一個實現了Runnable接口的參數。下面我們看看這個構造函數的具體實現:

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

調用init方法對線程的一些狀態優先級等做一個初始化的操作,我們順便看看使用第一種方式創建線程實例的那個無參的構造函數:

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

可以看到,兩個構造函數的內部調用的是同一個方法,只是傳入的參數不同而已。所以他們之間的區別就在于初始化的時候這個Runnable參數是否為空,當然這個參數的用處在run方法中也可以看出來:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

如果我們使用第二種方式構建Thread實例,那么此處的target肯定不會是null,自然會調用我們重寫的run方法。如果使用的是第一種方式構建的Thread實例,那么就不會調用上述的run方法,而是調用的我們重寫的Thread的run方法,所以從本質上看,兩種方式的底層處理都是一樣的。

這就是創建一個線程類并啟動該線程的兩種不同的方式,表面上略有不同,但是實際上都是一樣的調用init方法完成初始化。對于啟動線程的start方法的源碼,由于調用本地native方法,暫時并不易解釋,有興趣的可以使用jvm指令查看本地方法的實現以了解整個線程從分配資源到調用run方法啟動的全過程。

?????二、線程的多種狀態
?????線程是有狀態的,它會因為得不到鎖而阻塞處于BLOCKED狀態,會因為條件不足而等待處于WAITING狀態等。Thread中有一個枚舉類型囊括了所有的線程狀態:

public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

NEW狀態表示線程剛剛被定義,還未實際獲得資源以啟動,也就是還未調用start方法。

RUNNABLE表示線程當前處于運行狀態,當然也有可能由于時間片使用完了而等待CPU重新的調度。

BLOCKED表示線程在競爭某個鎖失敗時被置于阻塞狀態

WAITING和TIMED_WAITING表示線程在運行中由于缺少某個條件而不得不被置于條件等待隊列等待需要的條件或資源。

TERMINATED表示線程運行結束,當線程的run方法結束之后,該線程就會是TERMINATED狀態。

我們可以調用Thread的getState方法返回當前線程的狀態:

/*定義一個線程類*/
public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("myThread's state is : "+Thread.currentThread().getState());
    }
}
/*啟動線程*/
public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new MyThread());
        myThread.start();
        Thread.sleep(1000);
        System.out.println("myThread's state is : "+myThread.getState());
    }
這里寫圖片描述

我們兩次輸出myThread線程的當前狀態,在run方法中輸出結果顯示該線程狀態為RUNNABLE,當該run方法執行結束時候,我們又一次輸出該線程的當前狀態,結果顯示該線程處于TERMINATED。至于更加復雜的線程狀態,我們將在后續的文章中逐漸進行介紹。

?????三、Thread類中的其他一些常用屬性及方法
?????以上我們介紹了創建線程的兩種不同的方式以及線程的幾種不同狀態,有關于線程信息屬性的一些方法還沒有介紹。本小節將來簡單介紹下線程所具有的基本的一些屬性以及一些常用的方法。

首先每個線程都有一個id和一個name屬性,id是一個遞增的整數,每創建一個線程該id就會加一,該id的初始值是10,每創建一個線程就會往上加一。所以該id也間接的告訴了我們當前線程在所有線程中的位置。name屬性往往是以“Thread-”+編號作為某個具體線程的name值。例如:

public static void main(String[] args){
        for (int i=0;i<10;i++){
            Thread myThread = new Thread(new MyThread());
            myThread.start();
            System.out.println(myThread.getName());
        }
    }

輸出結果:

這里寫圖片描述

除此之外,Thread中還有一個屬性daemon,它是一個boolean類型的變量,該變量指示了當前線程是否是一個守護線程。守護線程主要用于輔助主線程完成工作,如果主線程執行結束,那么它的守護線程也會跟著結束。例如:我們的main程序在執行的時候,始終有一個垃圾回收線程作為守護線程輔助一些對象的回收工作,當main程序執行結束時,守護線程也將退出內存。關于守護線程有幾個方法:

public final boolean isDaemon() :判斷當前線程是否是守護線程

public final void setDaemon(boolean on):設置當前線程是否作為守護線程

還有一個方法較為常見,join。該方法可以讓一個線程等待另一個線程執行結束之后再繼續工作。例如:

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("myThread is running");
    }
}
public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.start();

        //主線程等待myThread線程執行結束
        myThread.join();

        System.out.println("waiting myThread done....");
    }

輸出結果:

這里寫圖片描述

有人可能會疑問,我們使用多線程不就是為了充分利用計算機資源,使其同時執行多個任務,為什么又要讓一個線程等待另一個線程呢?其實某些時候,主線程需要拿到所有分支線程計算的結果再一次進行計算,各個分支線程的進度各有快慢,主線程唯有等待他們全部執行結束之后才能繼續。此時就需要使用join方法了,所以說每一個方法的存在都有其可應用的場景。至于這個join的源代碼也是很有研究價值的,我們將在后續的文章中對其源代碼的實現進行進一步的學習。

還有一些屬性和方法,限于篇幅,本文不再繼續學習,大家可以自行查看源碼進行學習。下面我們看看多線程之后可能會遇到的幾個經典的問題。

?????四、多線程遇到的幾個典型的問題
?????第一個可能遇到的問題是,競態條件。也就是說,當多個線程同時訪問操作同一個對象的時候,最終的結果可能正確也可能不正確,具體的執行情況和線程實際的執行時序有關。
例如:

/*我們定義一個線程*/
public class MyThread implements Runnable{
    public static int count;

    @Override
    public void run(){
        try {
            Thread.currentThread().sleep((int)(Math.random()*100));
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/*main方法中啟動多個線程*/
public static void main(String[] args){

        Thread[] threads = new Thread[100];
        for (int i=0;i<100;i++){
            threads[i] = new Thread(new MyThread());
            threads[i].start();
        }

        for (int j =0;j<100;j++){
            threads[j].join();
        }

        System.out.println(MyThread.count);
    }

首先在我們自定義的線程類中,有一個static公共變量,而我們的run方法主要就做兩個事情,隨機睡一會和count增一。再來看main函數,首先定義了一百個線程并逐個啟動,然后主線程等待所有的子線程完成之后輸出count的值。

按照我們一般的思維,這一百個線程,每個線程都是為count加一,最后的輸出結果應該是100才對。但是實際上我們多次運行該程序得到的結果都是不一樣的,但幾乎都是小于100的。

這里寫圖片描述

這里寫圖片描述

這里寫圖片描述

為什么會出現這樣的情況呢?主要原因還是在于為count加一這個操作,它并非是原子操作,也就是說想要為count加一需要經過起碼兩個步驟:

  • 取count的當前值
  • 為count加一

因為每個線程都是隨機睡了一會,有可能兩個線程同時醒來,都獲取到當前的count的值,又同時為其加一,這樣就導致兩個不同的線程卻只為count增加了一次值。這種情況在多線程的前提下,發生的概率就更大了,所以這也是為什么我們得到的結果始終小于100但又每次都不同的原因。

第二個問題是,內存的可見性問題。就是說,如果兩個線程共享了同一個參數,其中一個線程對共享參數的修改而另一個線程并不會立馬能夠看到。原因是這些修改會被暫存在CPU緩存中,而沒有立馬寫回內存。例如:

public class MyThread extends Thread{
    public static boolean flag = false;

    @Override
    public void run(){
        while(!flag){
            //just running
        }
        System.out.println("my thread has finished ");
    }
}
public static void main(String[] args) throws InterruptedException {
        Thread myThread = new MyThread();
        myThread.start();

        Thread.sleep(1000);
        MyThread.flag = true;
        System.out.println("main thread has finished");
    }

首先我們定義一個線程類,該線程類中有一個靜態共享變量flag,run方法做的事情很簡單,死循環的做一些事情,等待外部線程更改flag的值,使其退出循環。而main方法首先啟動一個線程,然后修改共享變量flag的值,按照常理線程myThread在main線程修改flag變量的值之后將退出循環,打印退出信息。但是實際的輸出結果為:

這里寫圖片描述

main線程已經結束了,而整個程序并沒有結束,線程myThread的結束信息也沒有被打印,這就說明myThread線程還困在while循環中,但是實際上主線程已經將flag的值修改了,只是myThread無法看見。這是什么原因呢?

我們知道,每個線程都有一些緩存,往往為了效率,對一個變量值的修改并不會立馬寫會內存,而是注入緩存中,等到一定的時候才寫回內存,而當別的線程來修改這些共享的變量的時候,他們是從內存進行讀取的,修改后可能也沒有及時的寫回內存中,這就很容易導致其他線程根本就看不到你所做的修改。這就是典型的內存可見性問題。

本小節簡單的介紹了多線程的兩個典型的問題,解決辦法其實有多種,我們將在下篇文章中涉及。

以上的本篇內容主要介紹了線程的基本概念,如何創建一個線程,如何啟動一個線程,還有與線程相關的一些基本的屬性和方法,總結不到之處,望大家指出,相互學習。下篇文章將介紹一個用于解決多線程并發問題的關鍵字synchronized。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,492評論 1 15
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,986評論 1 18
  • 前面的幾篇文章主要介紹了線程的一些最基本的概念,包括線程的間的沖突及其解決辦法,以及線程間的協作機制。本篇主要來學...
    Single_YAM閱讀 482評論 0 3
  • 未有過的壓郁 總是在不知不覺間淪陷 不知是我太過于煽情還是現實真的總是會讓人感觸良多 或許 在一個壓郁...
    小丑模樣閱讀 210評論 0 0
  • 介紹對象,剛剛接到了之前鄰居家的電話,說給我介紹對象,其實吧我內心很拒絕,但是還是要禮貌啊,畢竟鄰居還是看得上我,...
    啊貴閱讀 197評論 0 0