Java多執行緒-基礎及實現

ZeroWM發表於2017-06-30

1. 什麼是執行緒

執行緒是程式內的執行單元


 


 

某個程式當中都有若干個執行緒。

執行緒是程式內的執行單元。

使用執行緒的原因是,程式的切換是非常重量級的操作,非常消耗資源。如果使用多程式,那麼併發數相對來說不會很高。而執行緒是更細小的排程單元,更加輕量級,所以執行緒會較為廣泛的用於併發設計。

在Java當中執行緒的概念和作業系統級別執行緒的概念是類似的。事實上,Jvm將會把Java中的執行緒對映到作業系統的執行緒區。

2. 執行緒的基本操作

2.1 執行緒狀態圖


上圖是Java中執行緒的基本操作。

當new出一個執行緒時,其實執行緒並沒有工作。它只是生成了一個實體,當你呼叫這個例項的start方法時,執行緒才真正地被啟動。啟動後到Runnable狀態,Runnable表示該執行緒的資源等等已經被準備好,已經可以執行了,但是並不表示一定在執行狀態,由於時間片輪轉,該執行緒也可能此時並沒有在執行。對於我們來說,該執行緒可以認為已經被執行了,但是是否真實執行,還得看物理cpu的排程。當執行緒任務執行結束後,執行緒就到了Terminated狀態。

有時候線上程的執行當中,不可避免的會申請某些鎖或某個物件的監視器,當無法獲取時,這個執行緒會被阻塞住,會被掛起,到了Blocked狀態。如果這個執行緒呼叫了wait方法,它就處於一個Waiting狀態。進入Waiting狀態的執行緒會等待其他執行緒給它notify,通知到之後由Waiting狀態又切換到Runnable狀態繼續執行。當然等待狀態有兩種,一種是無限期等待,直到被notify。一直則是有限期等待,比如等待10秒還是沒有被notify,則自動切換到Runnable狀態

2.2 新建執行緒

Thread thread = newThread();
thread.start();

這樣就開啟了一個執行緒。

有一點需要注意的是

Thread thread = newThread();
thread.run();

直接呼叫run方法是無法開啟一個新執行緒的。

start方法其實是在一個新的作業系統執行緒上面去呼叫run方法。換句話說,直接呼叫run方法而不是呼叫start方法的話,它並不會開啟新的執行緒,而是在呼叫run的當前的執行緒當中執行你的操作。

Thread thread = newThread("t1")
{
        @Override
        public voidrun()
        {
                //TODO Auto-generated methodstub
                System.out.println(Thread.currentThread().getName());
        }
};
thread.start();

如果呼叫start,則輸出是t1

Thread thread = newThread("t1")
{
        @Override
        public voidrun()
        {
                //TODO Auto-generated methodstub
                System.out.println(Thread.currentThread().getName());
        }
};
thread.run();

如果是run,則輸出main。(直接呼叫run其實就是一個普通的函式呼叫而已,並沒有達到多執行緒的作用)

run方法的實現有兩種方式

第一種方式,直接覆蓋run方法,就如剛剛程式碼中所示,最方便的用一個匿名類就可以實現。

Thread thread = newThread("t1")
{
        @Override
        public voidrun()
        {
                //TODO Auto-generated methodstub
                System.out.println(Thread.currentThread().getName());
        }
};

第二種方式

Thread t1=newThread(new CreateThread3());

CreateThread3()實現了Runnable介面。

在張孝祥的視訊中,推薦第二種方式,稱其更加物件導向。

2.3 終止執行緒

  • Thread.stop() 不推薦使用。它會釋放所有monitor

在原始碼中已經明確說明stop方法被Deprecated,在Javadoc中也說明了原因。

原因在於stop方法太過"暴力"了,無論執行緒執行到哪裡,它將會立即停止掉執行緒。


當寫執行緒得到鎖以後開始寫入資料,寫完id =1,在準備將name = 1時被stop,釋放鎖。讀執行緒獲得鎖進行讀操作,讀到的id為1,而name還是0,導致了資料不一致。

最重要的是這種錯誤不會丟擲異常,將很難被發現。

2.4 執行緒中斷

執行緒中斷有3種方法

public voidThread.interrupt() // 中斷執行緒
public boolean Thread.isInterrupted() // 判斷是否被中斷
public static boolean Thread.interrupted() // 判斷是否被中斷,並清除當前中斷狀態

什麼是執行緒中斷呢?

如果不瞭解Java的中斷機制,這樣的一種解釋極容易造成誤解,認為呼叫了執行緒的interrupt方法就一定會中斷執行緒。 

其實,Java的中斷是一種協作機制。也就是說呼叫執行緒物件的interrupt方法並不一定就中斷了正在執行的執行緒,它只是要求執行緒自己在合適的時機中斷自己。每個執行緒都有一個boolean的中斷狀態(不一定就是物件的屬性,事實上,該狀態也確實不是Thread的欄位),interrupt方法僅僅只是將該狀態置為true對於非阻塞中的執行緒,只是改變了中斷狀態, 即Thread.isInterrupted()將返回true,並不會使程式停止; 

public voidrun(){//執行緒t1
   while(true){
      Thread.yield();
   }
}
t1.interrupt();

這樣使執行緒t1中斷,是不會有效果的,只是更改了中斷狀態位。

如果希望非常優雅地終止這個執行緒,就該這樣做

public void run(){
    while(true)
    {
       if(Thread.currentThread().isInterrupted())
        {
          System.out.println("Interruted!");
           break;
        }
        Thread.yield();
    }
}

使用中斷,就對資料一致性有了一定的保證。

對於可取消的阻塞狀態中的執行緒,比如等待在這些函式上的執行緒, Thread.sleep(), Object.wait(), Thread.join(), 這個執行緒收到中斷訊號後,會丟擲InterruptedException, 同時會把中斷狀態置回為false.

對於取消阻塞狀態中的執行緒,可以這樣抒寫程式碼:

public voidrun(){
    while(true){
       if(Thread.currentThread().isInterrupted()){
            System.out.println("Interruted!");
            break;
        }
        try {
           Thread.sleep(2000);
        } catch (InterruptedException e){
          System.out.println("Interruted When Sleep");
          //設定中斷狀態,丟擲異常後會清除中斷標記位
          Thread.currentThread().interrupt();
        }
        Thread.yield();
    }
}

2.5 執行緒掛起

掛起(suspend)和繼續執行(resume)執行緒

  • suspend()不會釋放鎖
  • 如果加鎖發生在resume()之前 ,則死鎖發生

這兩個方法都是Deprecated方法,不推薦使用。

原因在於,suspend不釋放鎖,因此沒有執行緒可以訪問被它鎖住的臨界區資源,直到被其他執行緒resume。因為無法控制執行緒執行的先後順序,如果其他執行緒的resume方法先被執行,那則後執行的suspend,將一直佔有這把鎖,造成死鎖發生。

用以下程式碼來模擬這個場景

package test;

public classTest
{
        static Object u = newObject();
        static TestSuspendThread t1 =new TestSuspendThread("t1");
        static TestSuspendThread t2 =new TestSuspendThread("t2");

public static classTestSuspendThread extendsThread
        {
                publicTestSuspendThread(String name)
                {
                        setName(name);
                }

@Override
                publicvoidrun()
                {
                        synchronized(u)
                        {
                                System.out.println("in" +getName());
                                Thread.currentThread().suspend();
                        }
                }
        }

public static voidmain(String[] args) throwsInterruptedException
        {
                t1.start();
                Thread.sleep(100);
                t2.start();
                t1.resume();
                t2.resume();
                t1.join();
                t2.join();
        }
}

讓t1,t2同時爭奪一把鎖,爭奪到的執行緒suspend,然後再resume,按理來說,應該某個執行緒爭奪後被resume釋放了鎖,然後另一個執行緒爭奪掉鎖,再被resume。

結果輸出是:

in t1
in t2

說明兩個執行緒都爭奪到了鎖,但是控制檯的紅燈還是亮著的,說明t1,t2一定有執行緒沒有執行完。

2.6 join和yeild

yeild是個native靜態方法,這個方法是想把自己佔有的cpu時間釋放掉,然後和其他執行緒一起競爭(注意yeild的執行緒還是有可能爭奪到cpu,注意與sleep區別)。在javadoc中也說明了,yeild是個基本不會用到的方法,一般在debug和test中使用。

join方法的意思是等待其他執行緒結束,就如suspend那節的程式碼,想讓主執行緒等待t1,t2結束以後再結束。沒有結束的話,主執行緒就一直阻塞在那裡。

package test;

public classTest
{
        public volatile static int i =0;

public static classAddThread extendsThread
        {
                @Override
                publicvoidrun()
                {
                        for(i = 0; i < 10000000;i++)
                                ;
                }
        }

public static voidmain(String[] args) throwsInterruptedException
        {
                AddThreadat = new AddThread();
                at.start();
                at.join();
                System.out.println(i);
        }
}

如果把上述程式碼的at.join去掉,則主執行緒會直接執行結束,i的值會很小。如果有join,列印出的i的值一定是10000000。

那麼join是怎麼實現的呢?

join的本質 

while(isAlive())
{
   wait(0);
}

join()方法也可以傳遞一個時間,意為有限期地等待,超過了這個時間就自動喚醒。

這樣就有一個問題,誰來notify這個執行緒呢,在thread類中沒有地方呼叫了notify?

在javadoc中,找到了相關解釋。當一個執行緒執行完成終止後,將會呼叫notifyAll方法去喚醒等待在當前執行緒例項上的所有執行緒,這個操作是jvm自己完成的。

所以javadoc中還給了我們一個建議,不要使用wait和notify/notifyall線上程例項上。因為jvm會自己呼叫,有可能與你呼叫期望的結果不同。

3. 守護執行緒

  • 在後臺默默地完成一些系統性的服務,比如垃圾回收執行緒、JIT執行緒就可以理解為守護執行緒。
  • 當一個Java應用內,所有非守護程式都結束時,Java虛擬機器就會自然退出。

而Java中變成守護程式就相對簡單了。

Thread t=new DaemonT();
t.setDaemon(true);
t.start();

這樣就開啟了一個守護執行緒。

package test;

public classTest
{
        public static classDaemonThread extendsThread
        {
                @Override
                publicvoidrun()
                {
                        for(int i = 0; i < 10000000;i++)
                        {
                                System.out.println("hi");
                        }
                }
        }

public static voidmain(String[] args) throwsInterruptedException
        {
                DaemonThreaddt = new DaemonThread();
                dt.start();
        }
}

當執行緒dt不是一個守護執行緒時,在執行後,我們能看到控制檯輸出hi

當在start之前加入

dt.setDaemon(true);

控制檯就直接退出了,並沒有輸出。

4. 執行緒優先順序

Thread類中有3個變數定義了執行緒優先順序。

public final static intMIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

package test;

public classTest
{
        public static class HighextendsThread
        {
                staticint count =0;
                @Override
                publicvoidrun()
                {
                        while(true)
                        {
                                synchronized(Test.class)
                                {
                                        count++;
                                        if(count >10000000)
                                        {
                                                System.out.println("High");
                                                break;
                                        }
                                }
                        }
                }
        }
        public static class Low extendsThread
        {
                staticint count =0;
                @Override
                publicvoid run()
                {
                        while(true)
                        {
                                synchronized(Test.class)
                                {
                                        count++;
                                        if(count >10000000)
                                        {
                                                System.out.println("Low");
                                                break;
                                        }
                                }
                        }
                }
        }

public static voidmain(String[] args) throwsInterruptedException
        {
                Highhigh = new High();
                Lowlow = newLow();
                high.setPriority(Thread.MAX_PRIORITY);
                low.setPriority(Thread.MIN_PRIORITY);
                low.start();
                high.start();
        }
}

讓一個高優先順序的執行緒和低優先順序的執行緒同時爭奪一個鎖,看看哪個最先完成。

當然並不一定是高優先順序一定先完成。再多次執行後發現,高優先順序完成的概率比較大,但是低優先順序還是有可能先完成的。

相關文章