1. 程式人生 > >理解Java執行緒

理解Java執行緒

使用多執行緒的目的是更好的利用cpu資源,大部分多執行緒程式碼都可以用單執行緒來實現,但也有無法用單執行緒實現的,如:生產者消費者模型
下面對一些常用的概念進行區分:
多執行緒:指的是這個程式(一個程序)執行時產生了不止一個執行緒。
並行與併發:
並行:多個cpu例項或者多臺機器同時執行一段處理邏輯,真正的同時。
併發:通過cpu排程演算法,讓使用者看上去同時執行,實際上從cpu操作層面不是真正的同時。
執行緒安全:經常用來描繪一段程式碼。指在併發的情況之下,該程式碼經過多執行緒使用,執行緒的排程順序不影響任何結果。這個時候使用多執行緒,我們只需要關注系統的記憶體,cpu是不是夠用即可。若執行緒不安全則意味著執行緒排程順序影響最終結果。如轉賬操作。
同步:Java中的同步指的是通過人為的控制和排程,保證共享資源的多執行緒訪問成為執行緒安全,來保證結果的準確。如在轉賬中加入@synchronized關鍵字。在保證結果準確的同時,提高效能,才是優秀的程式。

執行緒的狀態

執行緒共有五種狀態,其狀態轉換如下圖所示:

  • 新建態(New):新建立的執行緒物件。
  • 就緒態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於“可執行執行緒池”中,變得可執行,只等待獲取CPU的使用權。 即在就緒狀態的程序除CPU之外,其它的執行所需資源都已全部獲得。
  • 執行態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
  • 死亡態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
  • 阻塞態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。

阻塞的情況分三種:

  1. 等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入“等待池”中。進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()或notifyAll()方法才能被喚醒。
  2. 同步阻塞:執行的執行緒在獲取物件的同步鎖時(Synchronized),同步鎖被釋放進入可執行狀態(Runnable),若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入“鎖池”中。
  3. 其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
    注:在runnable狀態的執行緒是處於被排程的執行緒,此時的排程順序是不一定的。Thread類中的yield方法可以讓一個running狀態的執行緒轉入runnable。

控制執行緒的基本方法

首先就要明確monitor的概念,Java中的每個物件都有一個監視器,來監測併發程式碼的重入。在非多執行緒編碼時該監視器不發揮作用,反之如果在synchronized 範圍內,監視器發揮作用。

sleep()

sleep()方法屬於Thread類,是一個靜態方法,主要的作用是讓當前執行緒停止執行,把cpu讓給其他執行緒執行,但不會釋放物件鎖和監控的狀態,到了指定時間後執行緒又會自動恢復執行狀態
Java有兩種sleep方法,一個只有一個毫秒引數,另一個有毫秒和納秒兩個引數
sleep(long millis)
sleep(long millis, int nanos)
注意:執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始執行。

wait()與notify()

wait()屬於Object類,與sleep()的區別是當前執行緒會釋放鎖,進入等待此物件的等待鎖定池。若執行緒A呼叫Obj.wait(),執行緒A就會停止執行,而轉為等待狀態。至於等待時間,看其他執行緒是否呼叫Obj.notify(),成為多個執行緒之間進行通訊的有手段。
注意:無論是wait()還是notify()都需要首先獲得目標的物件的一個監視器。wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某物件的監視器)。這意味著wait之後,其他執行緒可以進入同步塊執行。當某程式碼並不持有監視器的使用權時去wait或notify,會丟擲java.lang.IllegalMonitorStateException。也包括在synchronized塊中去呼叫另一個物件的wait/notify,因為不同物件的監視器不同,同樣會丟擲此異常。
"Synchronzied"是一種同步鎖。作用是實現執行緒間同步,對同步的程式碼加鎖,使得每一次,只能有一執行緒進入同步塊,從而保證執行緒間的安全性。它修飾的物件有以下幾種:

  • 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的部分,進入同步程式碼前要獲得給定物件的鎖
  • 修飾一個例項方法,進入同步程式碼前要獲得當前例項的鎖
  • 修飾一個靜態方法,進入同步程式碼前要獲得當前類的鎖
    public class Test {
         final static Object object=new Object();
         public static class Thread1 extends Thread{
             @Override
             public void run() {
                 synchronized (object) {
                     System.out.println("T1 開始");
                     try {
                         System.out.println("T1 等待");
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("T1 結束");
                }                   
             }   
         }
         public static class Thread2 extends Thread{
             @Override
             public void run() {
                 synchronized (object) {
                     System.out.println("T2 開始");
                     System.out.println("釋放一個執行緒");
                     object.notify();
                     System.out.println("T2 結束");
                 }                   
             }   
         }
         
         public static void main(String[] args) throws InterruptedException {
             Thread t1=new Thread1();
             Thread t2=new Thread2();
             t1.start();
             t2.start();
             t1.join();
             t2.join();
        }
    }

執行結果:

T1 開始
T1 等待
T2 開始
釋放一個執行緒
T2 結束
T1 結束

如下為生產者消費者模型:

public class Test {
    private final int MAX_SIZE = 100;
    private LinkedList<Object> list = new LinkedList<Object>();

    /**
     * 生產產品
     * @param producer
     */
    public void produce(String producer) {
        synchronized (list) {
            // 如果倉庫已滿
            while (list.size() == MAX_SIZE) {
                System.out.println("倉庫已滿,【"+producer+"】: 暫時不能執行生產任務!");
                try {
                    // 由於條件不滿足,生產阻塞
                    list.wait();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 生產產品            
            list.add(new Object());            
            System.out.println("【"+producer+"】:生產了一個產品\t【現倉儲量為】:" + list.size());
            list.notifyAll();
        }
    }
    /**
     * 消費產品
     * @param consumer
     */
    public void consume(String consumer) {
        synchronized (list) {
            //如果倉庫儲存量不足
            while (list.size()==0) {
                System.out.println("倉庫已空,【"+consumer+"】: 暫時不能執行消費任務!");
                try {
                    // 由於條件不滿足,消費阻塞
                    list.wait();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("【"+consumer+"】:消費了一個產品\t【現倉儲量為】:" + list.size());
            list.notifyAll();
        }
    }

    public static void main(String[] args){
        Test test = new Test();
        for(int i=1;i<6;i++){
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test.produce(String.format("生產者%d:", finalI));
                }
            }).start();
        }

        for(int i=1;i<4;i++){
            int finalI = i;
            new Thread(()-> test.consume(String.format("消費者%d:", finalI))).start();
        }
    }
}

執行結果:

倉庫已空,【消費者2:】: 暫時不能執行消費任務!
倉庫已空,【消費者1:】: 暫時不能執行消費任務!
【生產者4:】:生產了一個產品 【現倉儲量為】:1
【生產者1:】:生產了一個產品 【現倉儲量為】:2
【消費者3:】:消費了一個產品 【現倉儲量為】:1
【消費者1:】:消費了一個產品 【現倉儲量為】:0
【生產者2:】:生產了一個產品 【現倉儲量為】:1
【消費者2:】:消費了一個產品 【現倉儲量為】:0
【生產者5:】:生產了一個產品 【現倉儲量為】:1
【生產者3:】:生產了一個產品 【現倉儲量為】:2

join()

在某些情況下,子執行緒需要進行大量的耗時運算,主執行緒可能會在子執行緒執行結束之前結束,但是如果主執行緒又需要用到子執行緒的結果,換句話說,就是主執行緒需要在子執行緒執行之後再結束。這就需要用到join()方法

public class Test {
     public static int count;
        public static class AddThread implements Runnable{
             @Override
             public void run() {
                 for (int i = 0; i < 1000000000; i++) {
                     count++;
                 }   
             }   
        }
        public static void main(String[] args) throws InterruptedException {
            AddThread addThread=new AddThread();
            Thread t1=new Thread(addThread);
            t1.start();
            t1.join();
            System.out.println(count);
        }
}

yield()

一個執行緒呼叫yield()意味著告訴虛擬機器可以把自己的位置讓給其他執行緒(這只是暗示,並不表絕對)。但要注意,讓出cpu並不代表當前執行緒不執行了。當前執行緒讓出cpu後,還會進行cpu資源的爭奪,但是能不能再次分配到,就不一定了。使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。

volatile

多執行緒的記憶體模型:main memory(主存)、working memory(執行緒棧),在處理資料時,執行緒會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變數的操作都激發一次load and save)。
針對多執行緒使用的變數如果不是volatile或者final修飾的,很有可能產生不可預知的結果(另一個執行緒修改了這個值,但是之後在某執行緒看到的是修改之前的值)。其實道理上講同一例項的同一屬性本身只有一個副本。但是多執行緒是會快取值的,本質上,volatile就是不去快取,直接取值。線上程安全的情況下加volatile會犧牲效能。

執行緒建立

Java多執行緒實現方式主要有四種:繼承Thread類、實現Runnable介面、實現Callable介面通過FutureTask包裝器來建立Thread執行緒、使用ExecutorService、Callable、Future實現有返回結果的多執行緒。
其中,其中前兩種方式執行緒執行完後都沒有返回值,後兩種是帶返回值的。

繼承Thread類建立執行緒

Thread類本質上是實現了Runnable介面的一個例項,代表一個執行緒的例項。啟動執行緒的唯一方法就是通過Thread類的start()例項方法。start()方法是一個native方法,它將啟動一個新執行緒,並執行run()方法。這種方式實現多執行緒很簡單,通過自己的類直接extend Thread,並複寫run()方法,就可以啟動新執行緒並執行自己定義的run()方法。

public class MyThread extends Thread {  
    public void run() {  
        System.out.println("MyThread.run()");  
    }
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();  
        MyThread myThread2 = new MyThread();  
        myThread1.start();  
        myThread2.start(); 
    }
}  

實現Runnable介面建立執行緒

如果自己的類已經extends另一個類,就無法直接extends Thread,此時,可以實現一個Runnable介面,程式碼如下:

public class MyThread implements Runnable {  
    public void run() {  
        System.out.println("MyThread.run()");  
    }  
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();  
        MyThread myThread2 = new MyThread();  
        myThread1.start();  
        myThread2.start(); 
    }
}

實現Callable介面,通過FutureTask包裝器建立執行緒

/**
 * 實現Callable介面建立執行緒,相較於實現Runnable介面的方式,方法可以有返回值,並且可以丟擲異常
 * 執行Callable方式,需要FutureTask實現類的支援,用於接收運算結果
 */
public class Test {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
         // 執行Callable方式,需要FutureTask實現類的支援,用於接收運算結果
         FutureTask<Integer> result = new FutureTask<>(td);
         new Thread(result).start();

         // 接收執行緒運算後的結果
         Integer sum;
         try {
             //等所有執行緒執行完,獲取值,因此FutureTask 可用於 閉鎖
             sum = result.get();
             System.out.println("-----------------------------");
             System.out.println(sum);
         } catch (Exception e) {
             e.printStackTrace();
         }
    }
}

class ThreadDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        return sum;
    }
}

使用執行緒池建立返回結果的執行緒

使用ExecutorService、Callable、Future實現有返回結果的執行緒,ExecutorService、Callable、Future三個介面實際上都是屬於Executor框架。返回結果的執行緒是在JDK1.5中引入的新特徵,有了這種特徵可以很方便的得到返回值了,可返回值的任務必須實現Callable介面。類似的,無返回值的任務必須實現Runnable介面。
執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了。
注意:get方法是阻塞的,即:執行緒無返回結果,get方法會一直等待。再結合線程池介面ExecutorService就可以實現傳說中有返回結果的多執行緒了。

public class Demo {
    public static void main(String[] args) throws Exception {  
       System.out.println("----程式開始執行----");  
       Date date1 = new Date();  

       int taskSize = 5;  
       // 建立一個執行緒池  
       ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
       // 建立多個有返回值的任務  
       List<Future> list = new ArrayList<Future>();  
       for (int i = 0; i < taskSize; i++) {  
            Callable c = new MyCallable(i + " ");  
            // 執行任務並獲取Future物件  
            Future f = pool.submit(c);  
            // System.out.println(">>>" + f.get().toString());  
            list.add(f);  
       }  
       // 關閉執行緒池  
       pool.shutdown();  
       // 獲取所有併發任務的執行結果  
       for (Future f : list) {  
            // 從Future物件上獲取任務的返回值,並輸出到控制檯  
            System.out.println(">>>" + f.get().toString());  
       }  
       Date date2 = new Date();  
       System.out.println("----程式結束執行----,程式執行時間【"+ (date2.getTime() - date1.getTime()) + "毫秒】");  
    }  
}  

class MyCallable implements Callable<Object> {  
    private String taskNum;  
    MyCallable(String taskNum) {  
       this.taskNum = taskNum;  
    }  

    public Object call() throws Exception {  
       System.out.println(">>>" + taskNum + "任務啟動");  
       Date dateTmp1 = new Date();  
       Thread.sleep(1000);  
       Date dateTmp2 = new Date();  
       long time = dateTmp2.getTime() - dateTmp1.getTime();  
       System.out.println(">>>" + taskNum + "任務終止");  
       return taskNum + "任務返回執行結果,當前任務時間【" + time + "毫秒】";  
    }  
}  

執行結果:

----程式開始執行----
>>>0 任務啟動
>>>2 任務啟動
>>>4 任務啟動
>>>1 任務啟動
>>>3 任務啟動
>>>2 任務終止
>>>4 任務終止
>>>0 任務終止
>>>0 任務返回執行結果,當前任務時間【1000毫秒】
>>>1 任務終止
>>>3 任務終止
>>>1 任務返回執行結果,當前任務時間【1000毫秒】
>>>2 任務返回執行結果,當前任務時間【1000毫秒】
>>>3 任務返回執行結果,當前任務時間【1000毫秒】
>>>4 任務返回執行結果,當前任務時間【1000毫秒】
----程式結束執行----,程式執行時間【1074毫秒】

程式說明:
上述程式碼中Executors類,提供了一系列工廠方法用於建立執行緒池,返回的執行緒池都實現了ExecutorService介面。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目執行緒的執行緒池。
public static ExecutorService newCachedThreadPool()
建立一個可快取的執行緒池,呼叫execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。
public static ExecutorService newSingleThreadExecutor()
建立一個單執行緒化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用來替代Timer類。
ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor後臺執行緒池還沒有完成Callable的計算,這呼叫返回Future物件的get()方法,會阻塞直到計算完成。

本文參考了:
https://www.cnblogs.com/wxd0108/p/5479442.html
https://www.cnblogs.com/Ming8006/p/7243858.html
https://www.cnblogs.com/felixzh/p/6036074.html
https://www.cnblogs.com/ccfdod/p/6396012.html
http://www.cnblogs.com/jijijiefang/articles/7222955.html