1. 程式人生 > >死磕Java基礎--Java多執行緒那點事兒

死磕Java基礎--Java多執行緒那點事兒

1、執行緒和程序的區分

一個程序中包含多個執行緒,一個程序就相當於一個應用程式,一個應用程式底層就是cpu來執行的,比如我們的電腦同時打開了多個應用,表面看來像是在同時執行,實際上在同一時間只運行了一個應用程式,只不過cpu的執行速度非常快,會進行高速切換,讓我們覺得是在同時進行。

最經典的一個例子就是迅雷了,我們電腦開啟迅雷相當於開了一個程序,當我們使用迅雷下載東西的時候,比如說,下載兩部電影,那麼在迅雷中就存在兩個不同的執行路徑,也就是有兩個執行緒在同時做下載工作。

所以,程序包含執行緒,相當於所有執行緒的集合。一個執行緒就是一個執行路徑。

為什麼要用多執行緒?
多執行緒的好處就是提高程式的效率。但是可能會影響效能。

main是主執行緒,我們建立的執行緒叫做子執行緒,如果說一個專案中肯定有一個執行緒,那麼這個執行緒就是主執行緒了。

image

對於這個問題,到底該怎麼理解或者說區分執行緒和程序的概念,再舉一個非常貼切的例子,我們大多數人都用過扣扣吧,我們開啟一個扣扣,其實就是開啟了一個程序,然後我們傳送一段文字,那就是開啟了一個執行緒,我們再發送一天語音,那就是又開啟了一個執行緒,那麼在這個扣扣的程序中就有發文字和語言兩個執行緒了,當然,可能還有其他的執行緒!

在知乎上有這麼一個帖子,就是區分執行緒和程序的額,回答的也不少,可以看看

那麼,關於程序和執行緒的區別問題就到這!

2、多執行緒的建立方式

學習多執行緒,最基本的就是要會建立多執行緒了,常規來說建立多執行緒的方式應該有三種

  1. 繼承Thread類
  2. 實現Runnable介面
  3. 匿名內部類

在此之前很有必要說一下這個main,也就是在寫Java程式中經常見到的主執行緒,程式碼表現形式就是

    public static void main(String[] args){
        do...
    }

這個main叫做主執行緒,是程式的入口,而且是由jvm也就是Java虛擬機器建立的。

下面具體說一下建立執行緒的三種方式

首先是繼承自Thread類的方式,看程式碼

/**
 * 建立執行緒的我第一種方式
 * 繼承自Thread類
 */
class A extends Thread{ @Override public void run() { System.out.println("正在執行執行緒A。。。。"); } }

以上就是使用繼承自Thread類的方式建立執行緒,這裡的Thread實際上是實現了Runnable介面

image

再看這個Runnable介面

image

因此,使用繼承Thread類的方式建立執行緒需要實現run方法,實際的邏輯處理也是在這個run方法中實現的

再看第二種建立執行緒的方式就是實現Runnable介面的方式,同樣,先來看程式碼

/**
 * 建立執行緒的第二種方式
 * 實現Runnable介面
 */
class B implements Runnable {

    @Override
    public void run() {
        System.out.println("正在執行執行緒B、、、");
    }
}

之前就說過,Runnable介面中有一個抽象run方法,所以,對於實現Runnable介面的方式也是需要實現run方法的,同樣的邏輯處理也是在run中,接下來看最後一種建立執行緒的方式,通過匿名內部類的方式。

    public static void main(String[] args) {
        System.out.println("主執行緒在執行、、、、");


        /**
         * 執行緒建立的第三種方式
         * 匿名內部類
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名內部類執行的執行緒");
            }
        }).start();

    }

這裡要注意,匿名內部類是要寫在方法之中的,這裡寫在主方法中,可以看到,這個執行緒是通過新建一個Thread物件,然後在傳入一個Runnable,之後也是實現Run方法,然後呼叫執行緒的start方法即可開啟此執行緒

在最後一種使用匿名內部類的方式建立執行緒中呼叫了start開啟執行緒,那麼,對於其他兩種建立執行緒的方式該如何啟動執行緒呢?

 //執行執行緒B
        B b = new B();
        Thread thread = new Thread(b);
        thread.start();

        //執行執行緒A
        A a = new A();
        a.start();

可以看到,都是呼叫執行緒物件的start方法從而開啟執行緒,這裡有些人會有些疑問,我們隨便看一個


/**
 * 建立執行緒的我第一種方式
 * 繼承自Thread類
 */
class A extends Thread{
    @Override
    public void run() {
        System.out.println("正在執行執行緒A。。。。");
    }

}

就拿這個執行緒來說,為什麼不可以這樣

image

也就是說線上程物件中是有一個run方法的,為什麼執行執行緒不可以直接呼叫這個run方法呢?而要呼叫start開啟執行緒呢?

其實也很好理解,如果直接呼叫run方法的情況下,那麼這跟平常的類又有什麼區別呢?要知道執行緒是獨立的額一個執行緒,如果直接呼叫run方法的話不就等同於直接執行這個方法,就類似一個普通的類,然後呼叫它的一個方法似的,可是,這裡可是執行緒啊。

說的官方一點,對於執行緒而言,有一個執行緒規劃器的概念,可以理解為就是專門管理執行緒執行的一個玩意,只有當你呼叫start,才會將這個執行緒交給執行緒規劃器去管理,只有交給了執行緒規劃器,才能真正算得上是一個執行緒,否則,就是一個普通的類。而非執行緒。

以上說了建立執行緒的三種方式,那麼,到底使用哪種比較好呢?實際情況中可能使用實現Runnable介面的方式可能多一點,為什麼呢?

也很簡單,因為在Java中,類只能是單繼承的,所以如果使用繼承Tread類的方式的話就不能再繼承自其它的類了,這在實際的開發中勢必會帶來一些侷限性,但是使用實現Runnable介面的方式就可以避免這一侷限性,因為介面是支援多實現的,而且還可以再繼承其它的類,這樣的話,靈活性就高出很多。

到此要知道的幾個知識點

  1. 建立執行緒的三種方式
  2. 為什麼不呼叫run
  3. 使用哪種建立執行緒的方式更好,為什麼

再續

3、執行緒常用的API

先來看一段程式碼

image

在這段程式碼中,跟之前寫的建立執行緒程式碼沒什麼區別,就是在列印的時候添加了一個

Thread.currentThread().getName()

很好理解,就是得到當前執行緒的名稱的,看輸出結果

image

平常在開發當中,如果需要得知當前執行緒就可以使用此方法來獲得當前執行緒的名稱。

下面再介紹另外一個方法:isAlive()

這個方法是用來判斷當前執行緒是否處於活動狀態,那麼首先需要知道什麼是“活動狀態”

所謂的活動狀態就是執行緒已經啟動且尚未停止,根據這個理解,看一段程式碼,以一個執行緒為例

/**
 * 建立執行緒的第一種方式
 * 繼承自Thread類
 */
class A extends Thread{
    @Override
    public void run() {
        System.out.println("正在執行執行緒A。。。。"+Thread.currentThread().getName());
        System.out.println("執行緒的活動狀態是:"+Thread.currentThread().isAlive());

    }

}

然後執行

 //執行執行緒A
        A a = new A();
        System.out.println("此時執行緒的狀態是:"+Thread.currentThread().isAlive());
        a.start();
        System.out.println("此時執行緒的狀態是:"+Thread.currentThread().isAlive());

重點看一下執行結果

image

對照著程式碼再看執行結果,能夠看出對於執行緒a來說只有當呼叫了start方法,執行緒才開始執行,也就是處於活動狀態。

此外還有一個比較熟悉的方法就是sleep(),是讓執行緒暫時休眠的一個方法,這裡要注意的是是讓當前正在執行的執行緒暫停執行,下面看一個具體的例子

     //執行執行緒A
        A a = new A();
        a.start();
        System.out.println(System.currentTimeMillis());
        try {
            Thread.sleep(2000);
            System.out.println(System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

程式碼當中添加了以下程式碼來讓執行緒休眠2秒

hread.sleep(2000);

然後看列印輸出結果

image

執行緒在執行的過程中暫停了2000毫秒也就是2秒鐘的時間,這在平常的開發中也有一些特殊的用處,需要用到的時候能夠寫出來即可。

以上都是在介紹執行緒的一些常用API,其實還有一個也應該知曉,那就是getId(),這個是用來活的執行緒的唯一標示的,比如有如下用法

image

看列印輸出結果

image

得出的執行緒ID則可以作為判定此執行緒的唯一標示

關於執行緒的常用API就介紹以上這幾個,更多的可以等到需要用到的時候再針對的去查詢,對於以上常用的則需要記住。

4、執行緒的停止

對於執行緒,它有這樣的生命週期,就是新建、就緒、執行、阻塞和消亡

對於這幾種狀態也比較好理解,首先是

  1. 新建狀態:沒有呼叫satrt方法就處於新建狀態
  2. 就緒狀態:即使此時已經呼叫了start的方法,執行緒也不會立馬執行,必須等到jvm呼叫run方法執行緒才會真正的執行,而當前狀態則為就緒狀態
  3. 執行狀態:就是呼叫了run方法之後
  4. 阻塞狀態:在執行狀態如果呼叫了sleep方法就會處於阻塞狀態
  5. 消亡狀態:也就是執行緒被停止了

關於執行緒的這幾種狀態,要好好說一說的就是執行緒的停止了,因為關於執行緒的停止不是想象中的那樣,也許可以呼叫執行緒的stop方法將執行緒進行停止掉,但是,現如今,stop已經不推薦使用了,大多數停止一個執行緒將採用Thread.interrupt()這個方法。

而關於interript也不是想象中的那樣,只要呼叫了這個方法,執行緒就會停止,其實,呼叫了interrupt只相當於給當前執行緒打上了一個停止的標記,而此時,執行緒其實並沒有真正的停止,而這其中很明顯,缺少一些步驟。

來看一段程式碼

/**
 * 建立執行緒的我第一種方式
 * 繼承自Thread類
 */
class A extends Thread{
    @Override
    public void run() {
        System.out.println("正在執行執行緒A。。。。"+Thread.currentThread().getName());
        for (int i=0;i<10;i++){
            System.out.println(i);

        }

    }

}

//執行執行緒A
        A a = new A();
        a.start();
        a.interrupt();

如果看到上面的程式碼,會不會以為執行緒會被停止掉呢?實際答案是不會,以上並沒有真正的去停止執行緒,而是打上了一個停止的標記,那該怎麼做,這裡需要加上一個判斷

也就是說,有了如下執行執行緒的程式碼

   //執行執行緒A
        A a = new A();
        a.start();
        //此處打上一個停止的標記
        a.interrupt();

並且已經呼叫了interrupt,相當於已經給此執行緒打上了中斷的標誌,但是此時執行緒並沒有停止,還需要做如下的判斷

image

這裡使用到了這麼一句程式碼

this.isInterrupted()

這行程式碼代表著獲取執行緒的中斷標誌,簡單來說,如果在此之前你呼叫了interrupt的話它就返回true,否則就是false,所以就可以通過這種方式來達到停止執行緒的目的。

這個isInterrupted就是返回執行緒是否中斷的一個狀態,看執行結果

image

此時執行緒的中斷狀態是true,再看下面這種情況

image

這裡有個很重要的知識點,先看下輸出結果吧

image

可以看到,此時的中斷狀態變成了false,在之前明明已經為執行緒打上中斷標誌了,為什麼這裡變成了false,這是因為執行緒休眠的緣故,簡單來說,如果你讓執行緒進行休眠,就會丟擲一箇中斷異常,因為你之前打上了中斷標誌,所以呼叫sleep就會丟擲一箇中斷異常,而且還會將中斷標誌設定成false,對,就是這麼個道理,所以這裡的中斷標誌狀態才會是false。

繼續回到之前成功停止執行緒的程式碼上,也就是這些


/**
 * 建立執行緒的我第一種方式
 * 繼承自Thread類
 */
class A extends Thread{
    @Override
    public void run() {
        System.out.println("正在執行執行緒A。。。。"+Thread.currentThread().getName());
        for (int i=0;i<10;i++){
            if (this.isInterrupted()){
                System.out.println("執行緒已經停止");
                System.out.println("當前執行緒的狀態:"+this.isInterrupted());
                break;
            }
            System.out.println(i);

        }
    }

}

上面的做法貌似已經將執行緒停止掉了,但是事實真的是這樣嗎,小小的改動一下

image

看下列印結果

image

什麼意思呢?也就是說在for迴圈之後的程式碼還是會執行的,這樣來看,這個執行緒好像就沒有真的被停止掉,那麼,該怎麼處理這種情況呢?

/**
 * 建立執行緒的我第一種方式
 * 繼承自Thread類
 */
class A extends Thread{
    @Override
    public void run() {
        System.out.println("正在執行執行緒A。。。。"+Thread.currentThread().getName());
        try {
            for (int i=0;i<10;i++){
                if (this.isInterrupted()){
                    System.out.println("執行緒已經停止");
                    System.out.println("當前執行緒的狀態:"+this.isInterrupted());
                    throw new InterruptedException();
                }
                System.out.println(i);
            }
            System.out.println("此處還會被執行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

以上就是一個處理方法,我們在判斷到中斷標誌之後丟擲一箇中斷異常,然後再捕獲這個異常,這樣就能避免for迴圈之後的鱷魚局繼續被執行的情況,也就是真正的停止掉執行緒。

所以對於執行緒的停止,一個好的方法就是上面這種拋異常的方式了。

5、執行緒安全問題(synchronized)

提到執行緒安全問題,首先就要先回答一個問題,那就是為什麼會出現執行緒安全問題呢?

那麼為什麼會出現執行緒安全問題呢?簡單來說,及時多個執行緒同時訪問一個共享變數的情況下就會出現執行緒安全的問題,簡單來個例子看一下

class User{

    int age;

    public void setAge(int age0) {
        this.age = age0;
System.out.println("age="+age+"當前執行緒為:"+Thread.currentThread().getName());
    }


}

public class SynchronizedTest {

    public static void main(String[] args) {

        User user = new User();

        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                //線上程a中對user的age及逆行數值修改
                user.setAge(66);

            }
        });

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                //線上程b中對user的age及逆行數值修改
                user.setAge(88);
            }
        });


        //啟動執行緒
        a.start();
        a.setName("執行緒a");

        b.start();
        b.setName("執行緒b");

    }
}

這個時候執行我們的程式碼可能會出現這樣的問題

image

難道b執行緒中age不應該等於88嗎?怎麼都是66呢?這就出現了執行緒安全問題。

還可能會出現這樣的情況

image

這種情況就說明執行緒的實際執行順序並不一定按照程式碼書寫的順序。

而且還會出現這樣的問題

這裡也是發生了執行緒安全問題,那麼該如何解決這個執行緒安全問題呢?要解決這個問題還要明白兩個概念,那就是同步和非同步,那什麼是同步什麼又是非同步呢?

先來簡單分析一下上述程式碼為什麼會出現執行緒安全問題,其實很簡單,對於age是共享記憶體,兩個執行緒可以同時對它進行訪問,當執行緒a訪問它將它的數值修改成66的時候可能會出現的一種情況就是,執行緒a剛把age修改成66,執行緒b又把它修改成88了,導致讀取到的都是88,也就是說執行緒a修改完成age之後被執行緒b打斷了一下,沒有及時的去讀取到自己修改的值,而是讀取到了被執行緒b修改的值。

再想一下為什麼會出現這種情況呢?其實就是線上程a呼叫setAge之後,執行緒b又呼叫了這個setAge,因此發生執行緒安全問題,此時這個方法就是非同步的,也就是可以被兩個執行緒同時操作,如果這個setAge被執行緒a呼叫期間執行緒b不能呼叫,只有等執行緒a呼叫並且完成相關操作,執行緒b才能夠呼叫,此時這個setAge就是同步的,而且也不會發生執行緒安全問題了

那麼怎麼實現上述所說的呢?

可以這樣解決

image

也就是使用synchronized來修飾setAge,這樣的話當執行緒a呼叫setAge的時候就會把這個方法加鎖,此時setAge是被鎖住的,執行緒b是無法呼叫的,只有當執行緒a把setAge操作執行完成之後,鎖才會被開啟。

這就就是接下來要說的使用synchronized來同步方法從而解決執行緒安全問題

覺得以上的方法就是最優的了嗎?當然不是,想一下使用synchronized來同步方法也就相當於給這個方法加上一個鎖,不能同時被多個執行緒訪問,但是如果這個方法中含有耗時操作而這個耗時操作又是不涉及執行緒安全的,那麼使用synchronized來同步方法顯然降低了效能,那該怎麼解決這個問題呢?

解決的一個思路就是隻對引起執行緒安全的程式碼進行synchronized同步,可以這樣做

image

這就是使用synchronized來同步發生執行緒安全的程式碼塊,這裡要注意synchronized需要傳入一個物件,這個物件可以是任意物件,但是要保證這個物件是被多個執行緒共享的,如果把這個物件定義在了方法裡,那麼每個執行緒呼叫方法都會建立一個新的物件,如此一來,多個執行緒訪問的就不是同一個物件,因此,依然發生執行緒安全問題,如下圖程式碼操作就是錯誤的。

image

這裡再說一下這個synchronized,在上面的程式碼中,為解決執行緒安全問題,在setAge方法上加上了一個synchronized,代表著同步此方法,這樣一來,一旦某個執行緒呼叫這個方法,實際上因為synchronized的原因此執行緒就獲得了這個方法所在的物件的鎖,其他執行緒若想再次呼叫此方法則必須排隊等候,知道獲得鎖的執行緒執行完畢。

6、volatile關鍵字

首先要知道的就是volatile關鍵字的作用是什麼?volatile的作用是使得變數在多個執行緒之間可見。

這裡通過一個例子做講解,先建立一個執行緒

class ThreadDemo extends Thread{
    boolean tag = true;

    public void run() {

        System.out.println("執行緒開始執行。。。");
        while (tag){

        }
        System.out.println("執行緒執行結束。。。");
    }

    public void isRun(boolean tag){
        this.tag = tag;
    }
}

通過一個while迴圈來代表執行緒一直執行,然後通過isRun方法來控制執行緒的結束,接下來在主執行緒中這樣操作

public class VolatileDemo {


    public static void main(String[] args) throws InterruptedException {

        ThreadDemo t1 = new ThreadDemo();
        t1.start();
        Thread.sleep(300);
        t1.isRun(false);

    }

}

可以想一下,執行緒會停止嗎?實際執行的結果是不會,為什麼呢?結合這張圖來說明一下

image

這裡的tag就是一個共享變數,首先子執行緒讀取到的是在子執行緒中的本地記憶體中的共享變數副本,雖然在主執行緒中通過isRun方法將tag變成false,但是子執行緒中讀取到的依然存放在本地記憶體中的副本依然是ture,也就是說通過isRun已經將主記憶體中的共享變數tag重新整理成false,但是子執行緒並沒有在主記憶體中讀取這個重新整理後的值,所以執行緒不會停止,那麼如何解決這個問題呢?

可以這樣做

image

對tag使用volatile關鍵字,這樣的話再次執行這個程式就會發現執行緒立馬就結束了,這是因為一旦tag加上vola關鍵字,就強制要求每次使用tag都必須從主記憶體中取值,因此子執行緒可以拿到主記憶體中最新更新的tag也就是false,執行緒就自然而然的停止了。

原因:
執行緒之間是不可見的,讀取的是副本,沒有及時讀取到主記憶體結果,解決辦法是使用volatile關鍵字解決執行緒之間的可見性,強制執行緒每次讀取該值的時候都去主記憶體中讀取。

7、執行緒之間的通訊wait和notify(一)

執行緒不是一個個獨立的個體,執行緒與執行緒之間是可以進行互相通訊和協作的。那麼關於執行緒之間的通訊,主要學習的就是等待和通知了,什麼意思呢?也就是說在學習執行緒之間的通訊這塊,可能打交道最多的就是wait()和notify()這兩個方法了。

那麼什麼是wait和notify,從字面意思理解就是等待和通知的意思,那麼在多執行緒中又是怎樣的呢?

線上程之間的通訊中,wait就代表讓此執行緒進入等待狀態,之後的程式碼將不再執行,這裡有一個前提就是,必須是在獲得同步鎖的前提下,Java為每一個Object物件都實現了wait和notify,但是不是隨隨便便就能呼叫的,比如這樣

 public static void main(String[] args) {
        String s = new String();
        try {
            s.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

也就是說,在Java中任何一個物件都是可以呼叫這個wait和notify的,不過,能呼叫歸能呼叫,出不出錯就是另外一回事,例如上面這段程式碼,呼叫了wait這個方法,然後執行

image

結果出錯,這是為什麼?因為在使用wait和notify是有一個前提的,那就是事先必須已經獲得了同步鎖,以下才是正確的使用方式

   public static void main(String[] args) {

        try {
            String s = new String();
            synchronized (s){
                s.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

以上再次執行就不會報錯了,因為已經加上了同步鎖,這個只是為了說明對於wait和notify呼叫的前提是必須已經獲得同步鎖。

也許還不是很清楚,那就記住一句話:只能在同步程式碼塊和同步方法中呼叫wait和notify。

接下來繼續看一個例子

public class Thread2 {

    public static void main(String[] args) {
        Object lock = new Object();
        MyThread myThread = new MyThread(lock);
        Thread thread = new Thread(myThread);
        thread.start();
        thread.setName("執行緒A");

    }
}

class MyThread implements Runnable{
private Object lock;
public MyThread(Object lock){
    this.lock = lock;
}
    @Override
    public void run() {
        System.out.println("執行緒開始工作"+Thread.currentThread().getName());
        synchronized (lock){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("執行緒結束工作"+Thread.currentThread().getName());
    }
}

首先從自定義的MyThread執行緒開始看,首先建立了一個object物件作為鎖物件,然後在同步程式碼塊中呼叫了wait方法,呼叫此方法的目的是讓此執行緒處於等待狀態,這樣一來,之後的程式碼就不會再執行,同時呼叫此wait方法將執行緒置於等待狀態的時候已經釋放了持有的鎖,先看一下以上程式碼的執行結果吧

wait

可以看得到,此時程式處於執行中狀態,這是因為自定義的執行緒被處於等待佇列的原因

那麼如何讓這個執行緒繼續執行剩下的程式碼呢?那就要使用notify這個方法了,再建立一個執行緒

class OtherThread extends Thread{
    private Object lock;
    public OtherThread(Object lock){
        this.lock = lock;
    }
    @Override
    public void run() {
        System.out.println("執行緒開始工作"+Thread.currentThread().getName());
        synchronized (lock){
            lock.notify();
        }
        System.out.println("執行緒結束工作"+Thread.currentThread().getName());
    }
}

這裡再同步程式碼塊中則呼叫了notify來發起一個通知,發起一個什麼通知呢?就是通知到那些呼叫了wait處於等待狀態的執行緒,告訴他們你們可以執行啦,並且呼叫了notify的執行緒不會像呼叫了wait的執行緒那樣立馬釋放掉鎖,而是會將執行緒執行完畢才會釋放鎖,然後之前處於等待狀態的執行緒拿到釋放的鎖繼續執行剩下的程式碼,所以這裡就有一個知識點,那就是這個鎖必須是同一個鎖,保證是同一個鎖的關鍵點就是這些程式碼了

private Object lock;
    public OtherThread(Object lock){
        this.lock = lock;
    }

然後執行這個執行緒

  public static void main(String[] args) {
        //同一個鎖
        Object lock = new Object();

        MyThread myThread = new MyThread(lock);
        Thread thread = new Thread(myThread);
        thread.start();
        thread.setName("執行緒A");

        OtherThread otherThread = new OtherThread(lock);
        otherThread.start();
        otherThread.setName("執行緒B");

    }

緊接著執行程式

image

結果正如分析的一樣!

以上只是等待通知的一種情況,那就是一個執行緒處於等待狀態,然後一個執行緒發起通知,緊接著處於等待的那個執行緒拿到發起通知的執行緒的鎖,繼而繼續執行。

當然,還有這麼一種情況,那就是有很多個執行緒處於等待狀態,然後一個執行緒發起通知,這樣的話就會隨機通知處於等待狀態中的一個執行緒,其實還有一個方法叫做notifyAll是用來通知所有的等待執行緒的,這樣的貨,處於等待狀態的這些個執行緒,誰的優先順序高,誰就會得到這個通知,從而拿到鎖。

8、執行緒間通訊join(二)

首先來說這個jon有什麼用,join是一個方法,執行緒可以呼叫這個方法,當在主執行緒中執行一個子執行緒,如果這個子執行緒執行結束會得到一個值,而在主執行緒中會用到這個值,但是實際的情況是,很有可能主執行緒已經執行完了,子執行緒還在執行,這樣就無法得到這個值了,該怎麼辦

使用join就可以解決這個問題,只需要讓子執行緒呼叫join方法,這樣就會在執行完子執行緒之後才會執行主執行緒,說簡單點,就是子執行緒呼叫了join方法之後,就必須等子執行緒執行完成之後才能幹其他的事

使用起來也很簡單

thread.join();

另外對於join還有這樣的寫法就是join(long),在程式碼中的表現形式就是如下

join(2000);

這樣的話就會使得執行緒等待2秒之後執行,要知道單獨使用這樣的程式碼

thread.join();

是必須等到子執行緒結束之後才會執行其他的程式碼,但是如果是這樣的話

join(2000);

兩秒之後就會執行其它的程式碼了,看到這裡,這個功能似乎跟這個有點像

Thread.sleep(2000);

但是兩者有個本質上的區別就是join的話會釋放鎖,而sleep則不會,這個在具體的場景中則會有具體的應用。

9、Lock的使用

之前講過使用synchronized關鍵字可以實現執行緒之間的同步,防止執行緒安全問題的產生,隨著jdk版本的不斷提升,在jdk1.5中出現了一個ReentrantLock也能實現相同的功能那個,而且比synchronized更加強大。

使用ReentrantLock實現同步會更加的好理解,看程式碼

class User{
    int age;
    public void setAge(int age0) {
        synchronized (this){
            this.age = age0;
            System.out.println("age="+age+"當前執行緒為:"+Thread.currentThread().getName());
        }
    }
}

之前是使用synchronized關鍵字來同步程式碼塊保證執行緒之間的同步,以防止發生執行緒安全問題,那麼該如何使用ReentrantLock來實現執行緒的同步呢

class User{
    private Lock lock = new ReentrantLock();
    int age;
    public void setAge(int age0) {
        lock.lock();
            this.age = age0;
            System.out.println("age="+age+"當前執行緒為:"+Thread.currentThread().getName());
        lock.unlock();
    }
}

以上就是使用ReentrantLock來實現執行緒之間的同步了,可以看到,使用ReentrantLock是很好理解的,首先建立一個鎖物件,然後上鎖,執行相關程式碼,然後再釋放鎖。

之前在講synchronized的時候,可以通過wait和notify實現等待/通知模式,現在使用ReentrantLock同樣可以完成等待通知模式,只不過這個要藉助一個Condition類,具體的做法如下

先看之前使用synchronized配合wait和notify實現的等待/通知模式

public class Thread2 {

    public static void main(String[] args) {
        Object lock = new Object();

        MyThread myThread = new MyThread(lock);
        Thread thread = new Thread(myThread);
        thread.start();
        thread.setName("執行緒A");



        OtherThread otherThread = new OtherThread(lock);
        otherThread.start();
        otherThread.setName("執行緒B");

    }
}

class MyThread implements Runnable{
private Object lock;
public MyThread(Object lock){
    this.lock = lock;
}
    @Override
    public void run() {
        System.out.println("執行緒開始工作"+Thread.currentThread().getName());
        synchronized (lock){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("執行緒結束工作"+Thread.currentThread().getName());
    }
}

class OtherThread extends Thread{
    private Object lock;
    public OtherThread(Object lock){
        this.lock = lock;
    }
    @Override
    public void run() {
        System.out.println("執行緒開始工作"+Thread.currentThread().getName());
        synchronized (lock){
            lock.notify();
        }
        System.out.println("執行緒結束工作"+Thread.currentThread().getName());
    }
}

這裡要把握一個重點就是在等待/通知模式中是需要共同的鎖,所以在子執行緒中需要得到相同的鎖,那麼繼續看使用ReentrantLock如何實現等待/通知模式,同樣,把握一個重點,相同的鎖,這裡還需要一個相同的Condition物件

首先建立兩個執行緒,一個等待,一個通知

class MyThead1 extends Thread{
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public MyThead1(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {

        try {
            lock.lock();
            System.out.println("執行緒開始工作"+Thread.currentThread().getName());
            condition.await();
            System.out.println("執行緒停止工作"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
class OtherThread1 extends Thread{
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public OtherThread1(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {
        lock.lock();
        System.out.println("執行緒開始執行"+Thread.currentThread().getName());
        condition.signal();
        System.out.println("執行緒停止執行"+Thread.currentThread().getName());
        lock.unlock();
    }
}

然後為了保證是同一把鎖和Condition物件,這樣操作

private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public MyThead1(Lock lock , Condition condition){
        this.lock = lock;
        this.condition = condition;
    }

接下來在主程式中呼叫測試

 Lock lock = new ReentrantLock();
         Condition condition = lock.newCondition();

        MyThead1 myThead1 = new MyThead1(lock,condition);
        myThead1.start();
        myThead1.setName("執行緒A");

看執行結果

image

果然,等待執行緒處於了等待狀態,此時可以看到執行緒並未停止,而是處於執行狀態,下面再呼叫通知執行緒檢視結果

 Lock lock = new ReentrantLock();
         Condition condition = lock.newCondition();

        MyThead1 myThead1 = new MyThead1(lock,condition);
        myThead1.start();
        myThead1.setName("執行緒A");

        OtherThread1 otherThread1 = new OtherThread1(lock,condition);
        otherThread1.start();
        otherThread1.setName("執行緒B");

看執行結果

image

可以看到,收到通知後,之前處於等待狀態的執行緒繼續執行了。

在這裡就需要知道這麼幾件事了

  1. 在等待/通知模式中,同一把鎖很重要
  2. wait就相當於Condition中的await
  3. notify就相當於Condition中的signal
  4. notifyAll就相當於signalAll

10、定時器Timer

Java中多執行緒定時器的作用就是可以讓一段程式碼持續性執行或者在規定的時間之後執行。

先來看定時器的用法

new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定時器一秒之後執行的");
            }
        },1000);

以上程式碼可以直接放在主執行緒中執行,也就是一秒鐘之後執行run中的程式碼,還可以這樣操作

看執行結果

image

  new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定時器一秒之後執行的");
            }
        },1000,1000);

這樣的話就代表一秒鐘之後執行run中的程式碼,然後每隔一秒重複執行一次,這樣就實現迴圈。

看執行程式碼

image

解釋以上程式碼中的一些知識

Timer類的主要作用就是設定計劃任務,但封裝任務的類卻是TimerTask類,Timer實際上是一個工具類,然後可以呼叫schedule來執行相關的定時任務

在第一個程式中實際上用到了這個
schedule (TimerTask task, Date time)

可以看到,對於這個schedule需要傳入一個TimerTask,還需要一個時間,這裡的意思就是經過多少時間之後執行這個任務。

對於第二個程式是這樣的schedule(TimerTack task, Date firstTime, long period)

相比於第一個在schedule中又多傳入一個值,這個值就代表經過這個時間之後再次執行這個任務,相當於無限迴圈執行。

實際的效果自己測試一下就會明瞭。

相關推薦

Java基礎--Java執行事兒

1、執行緒和程序的區分 一個程序中包含多個執行緒,一個程序就相當於一個應用程式,一個應用程式底層就是cpu來執行的,比如我們的電腦同時打開了多個應用,表面看來像是在同時執行,實際上在同一時間只運行了一個應用程式,只不過cpu的執行速度非常快,會進行高速切換,讓

java執行事兒

前段時間應隔壁部門大佬的邀約,簡單地幫他們部門的童靴梳理了下多執行緒相關的內容,客串了一把講師【因為部門內有不少是c#轉java的童鞋,所以講的稍微淺顯了些】 ok,按照個人習慣先來大綱 知識點: 1)程序 多執行緒的相關概念 涉及到CPU排程 稍微談下JVM記憶體模型 程式計數器 2)多執行緒的三種實

JAVA基礎23-執行(三)【synchronized,ReentranLock,volatile、鎖】

一、同步         大多數多執行緒應用中,兩個或兩個以上的執行緒需要共享對同一資料的存取,此時出現多個程式交替處理該資料,從而導致資料出現訛誤。 9-1.Synchronized關鍵字       &nb

Java基礎執行及併發庫

實際上關於多執行緒的基礎知識,前面自己已經總結過一部分,但是每一個階段對於同樣知識點的學習側重點是不一樣的,前面的Java基礎總結八之多執行緒(一)和 Java基礎總結九之多執行緒(二)是對JDK5以前多執行緒相關基礎知識的一個簡單總結,今天本文將偏重於JDK5提供的併發庫進行學習總結。 首先,

黑馬程式設計師----Java基礎執行

------- <a href="http://www.itheima.com" target="blank">android培訓</a>、<a href="http://www.itheima.com" target="blank">java培訓</a&g

Java基礎執行-生產消費

使用場景請看上一篇部落格Java基礎之多執行緒-多生產多消費 話不多說,直接上程式碼: 顧客: package cn.itcast.day07.demo02; public class MultiConsumer implements Runnable { priva

Java基礎執行案例-單生產單消費

在講單生產單消費之前,我們先來說一下執行緒間通訊的問題 一、 執行緒間通訊 概念:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。 比如:執行緒A用來生成包子的,執行緒B用來吃包子的,包子可以理解為同一資源,執行緒A與執行緒B處理的動作,一個是生產,一個是消費,

Java基礎執行之原理、實現方式及匿名內部類建立執行方法

一、概念 程序:作業系統當中正在執行的一個程式。例如正在執行一個QQ。 執行緒:程序之內多工的執行單位。例如迅雷當中正在下載的多個電影。 JVM當中:棧(Stack)記憶體是執行緒獨立的,堆(Heap)記憶體是執行緒共享的。 (1)Java程式執行的時候至少有兩個執行緒: 1)主

Java基礎學習——執行執行

1.執行緒池介紹     執行緒池是一種執行緒使用模式。執行緒由於具有空閒(eg:等待返回值)和繁忙這種不同狀態,當數量過多時其建立、銷燬、排程等都會帶來開銷。執行緒池維護了多個執行緒,當分配可併發執行的任務時,它負責排程執行緒執行工作,執行完畢後執行緒不關閉而是返回執行緒池,

sincerit java基礎執行

執行緒狀態 Java執行緒具有五中基本狀態 新建狀態(New):當執行緒物件對建立後,即進入了新建狀態,如:Thread t = new MyThread(); 就緒狀態(Runnable):當呼叫執行緒物件的start()方法(t.start();),執行緒即進入就緒狀態。處於就

Java基礎執行

以下是我們Java基礎多執行緒的一些知識點總結: 執行緒中run()和start()的區別:   對於Thread物件來說,當你呼叫的是start(),執行緒會被放到等待佇列,等待CPU排程,不一定馬上執行;無需等待run()方法執行完畢,可以直接執行下面的程式碼; 而呼叫的是run()的話,就是當做普通的方

Java基礎14--執行

14-1,多執行緒-執行緒間通訊示例 1,什麼是多執行緒間通訊? 多執行緒通訊:多個執行緒在處理同一個資源,但是任務不同。 比如說:有一堆煤(資源),一輛卡車向裡放煤(Input),一輛卡車向外取煤(output),放煤和取煤的任務不同,但操作的是同一個資源。 由於有兩個任務,所

JAVA基礎24-執行(四)【讀寫鎖,阻塞佇列,執行池】

一、讀寫鎖   使用步驟 二、阻塞佇列 (BlockingQueue) 提供執行緒安全的佇列訪問方式; 當阻塞佇列進行插入資料時,若佇列滿,則執行緒阻塞,直到佇列非滿的時候 當阻塞佇列取資料時,若佇列為空,則執行緒阻塞直到佇列非空時候。

JAVA基礎23-執行(二)【執行區域性變數和未捕獲異常處理器】

一、執行緒區域性變數    線上程中使用共享變數肯定是存在風險。為了規避這個風險,利用同步機制,volatile這些方法都可以。但是也可為每個執行緒分配一個變數。使用ThreadLocal輔助類為各個執行緒提供各自的例項。  ThreadLocal為每個使用

JAVA基礎22-執行(一)【執行的概念,執行狀態及其轉換】

多執行緒 一.概念          1. 程序:程序是表示資源分配的的基本概念,又是排程執行的基本單位,是系統中的併發執行的單位。           2. 執行緒:單個程序中執行中

java基礎執行匿名內部類和lambda建立方式,及執行中的兩個面試題

一、可以用匿名類和lambda兩個種方式建立多執行緒。 1.利用匿名內部類建立多執行緒並開啟。 new Thread() {//建立方式1 public void run() { for(int x=0; x<50; x++) { System.out

java基礎--24.執行的應用--電影院賣票程式的實現

A.多執行緒應用–電影院賣票程式的實現 A:繼承Thread類 B:實現Runnable介面 電影院賣票程式出問題 為了更符合真實的場景,加入了休眠100毫秒。 多執行緒賣票過程中可能出現的問題: a:同一張票多次出售

Java基礎執行執行

1.執行緒池的概念與Executors類的應用     應用需求:如果訪問伺服器的客戶端很多,那麼伺服器要不斷的建立和銷燬執行緒,這將嚴重的影響伺服器的效能     解決方法:首先建立一些執行緒,它

java基礎之——執行

程式碼體現: public class MyThread extends Thread { public void run() { for(int x=0; x<100; x++) { System.out.println(getName()+"--

java基礎24_執行

1.多執行緒: (1)如果一個應用程式有多條執行路徑,則被稱為多執行緒程式。概念區分:程序(正在執行的程式)、執行緒(程式的執行路徑,執行單元)、單執行緒(應用程式只有一條執行路徑)、多執行緒(應用程式有多條執行路徑)。 (2)多執行緒程式實現的兩種方案: --------