1. 程式人生 > >多執行緒入門

多執行緒入門

什麼是程序和執行緒?

一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。

多執行緒是多工的一種特別的形式,但多執行緒使用了更小的資源開銷。

一個程序包括由作業系統分配的記憶體空間,包含一個或多個執行緒。一個執行緒不能獨立的存在,它必須是程序的一部分。一個程序一直執行,直到所有的非守護執行緒都結束執行後才能結束。

為什麼使用多執行緒?

使用多執行緒可以編寫高效率的程式來達到充分利用 CPU,可以大大提高系統整體的併發能力以及效能.

執行緒的生命週期

執行緒是一個動態執行的過程,它也有一個從產生到死亡的過程。

下圖顯示了一個執行緒完整的生命週期。

  • 新建狀態:
    使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒.
  • 就緒狀態:
    當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。
  • 執行狀態:
    如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
  • 阻塞狀態:
    如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
    • 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
    • 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
    • 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
  • 死亡狀態:
    一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

建立執行緒的方式

java 提供了三種建立執行緒的方法:實現 Runnable 介面;繼承 Thread 類本身;通過 Callable 和 Future 建立執行緒。

1.實現 Runnable 介面

建立一個執行緒,最簡單的方法是建立一個實現 Runnable 介面的類,同時重寫 run()方法.

然後建立Runnable實現類的例項,並以此例項作為Thread的target物件,即該Thread物件才是真正的執行緒物件。

新執行緒建立之後,你呼叫它的 start() 方法它才會執行。

package com.example.test;

/**
 * @author ydx
 */
public class RunnableDemo implements Runnable{

    /**
     * 執行緒名稱
     */
    private String threadName;

    /**
     * 構造方法
     * @param threadName 執行緒名稱
     */
    public RunnableDemo(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        System.out.println(threadName + " is running");
        //業務
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + " 執行 " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(threadName + " is exiting");
    }

    public static void main(String[] args) {

        RunnableDemo runnable1 = new RunnableDemo("thread-1");
        Thread thread1 = new Thread(runnable1);
        thread1.start();

        RunnableDemo runnable2 = new RunnableDemo("thread-2");
        Thread thread2 = new Thread(runnable2);
        thread2.start();

    }
}

第一次執行結果如下:

thread-1 is running
thread-1 執行 0
thread-2 is running
thread-2 執行 0
thread-2 執行 1
thread-1 執行 1
thread-2 執行 2
thread-1 執行 2
thread-1 執行 3
thread-2 執行 3
thread-1 執行 4
thread-2 執行 4
thread-2 is exiting
thread-1 is exiting

第二次執行結果如下:

thread-1 is running
thread-1 執行 0
thread-2 is running
thread-2 執行 0
thread-1 執行 1
thread-2 執行 1
thread-1 執行 2
thread-2 執行 2
thread-1 執行 3
thread-2 執行 3
thread-2 執行 4
thread-1 執行 4
thread-1 is exiting
thread-2 is exiting  

可以看出兩次執行結果是不一樣的,每次兩個執行緒的執行順序是隨機的.

2.繼承Thread類

建立一個執行緒的第二種方法是建立一個新的類,該類繼承 Thread 類,然後建立一個該類的例項。

繼承類必須重寫 run() 方法,該方法是新執行緒的入口點。它也必須呼叫 start() 方法才能執行。

package com.example.test;

/**
 * @author ydx
 */
public class ThreadDemo extends Thread {

    /**
     * 執行緒名稱
     */
    private String threadName;

    public ThreadDemo(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public synchronized void start() {
        System.out.println(threadName+ " is starting......");
        super.start();
    }

    @Override
    public void run() {
        System.out.println(threadName + " is running");
        //業務
        for (int i = 0; i < 3; i++) {
            System.out.println(threadName + " 執行 " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(threadName + " is exiting");
    }

    public static void main(String[] args) {
        ThreadDemo thread1 = new ThreadDemo("thread-1");
        thread1.start();

        ThreadDemo thread2 = new ThreadDemo("thread-2");
        thread2.start();

    }
}

執行結果如下:

thread-1 is starting......
thread-2 is starting......
thread-1 is running
thread-1 執行 0
thread-2 is running
thread-2 執行 0
thread-1 執行 1
thread-2 執行 1
thread-2 執行 2
thread-1 執行 2
thread-2 is exiting
thread-1 is exiting   

3.通過 Callable 和 Future 建立執行緒

  1. 建立 Callable 介面的實現類,並實現 call() 方法,該 call() 方法將作為執行緒執行體,並且有返回值。
  2. 建立 Callable 實現類的例項,使用 FutureTask 類來包裝 Callable 物件,該 FutureTask 物件封裝了該 Callable 物件的 call() 方法的返回值。
  3. 使用 FutureTask 物件作為 Thread 物件的 target 建立並啟動新執行緒。
  4. 呼叫 FutureTask 物件的 get() 方法來獲得子執行緒執行結束後的返回值。

    package com.example.test;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * @author ydx
     */
    public class CallableTest implements Callable<Integer> {
    
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 0; i < 5; i++) {
                sum += i;
                System.out.println(i);
                //sleep 200ms
                Thread.sleep(200);
            }
            return sum;
        }
    
        public static void main(String[] args) {
    
            long start = System.currentTimeMillis();
    
            CallableTest callableTest = new CallableTest();
            FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
            new Thread(futureTask, "thread-1").start();
    
            CallableTest callableTest2 = new CallableTest();
            FutureTask<Integer> futureTask2 = new FutureTask<>(callableTest2);
            new Thread(futureTask2, "thread-2").start();
            try {
                System.out.println("thread-1的結果: " + futureTask.get());
                System.out.println("thread-2的結果: " + futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
            long end = System.currentTimeMillis();
            System.out.println("耗時: " + (end - start) + "ms");
        }
    }
    

    執行結果:

    0
    0
    1
    1
    2
    2
    3
    3
    4
    4
    thread-1的結果: 10
    thread-2的結果: 10
    耗時: 1004ms
    

    我們建立了兩個執行緒, 每個執行緒計算0~4的和,單個執行緒耗時200ms * 5 = 1000ms,而最終兩個執行緒的總耗時約1000ms,由此可見兩個執行緒是併發進行.

4.建立執行緒的三種方式的對比

  • 使用繼承 Thread 類的方式建立多執行緒時,編寫簡單,但是不夠靈活
  • 採用實現 Runnable、Callable 介面的方式建立多執行緒時,執行緒類只是實現了 Runnable 介面或 Callable 介面,還可以繼承其他類,建立執行緒比較靈活.

執行緒管理

Java提供了一些方法用於執行緒狀態的控制。具體如下:

1.sleep(執行緒睡眠)

如果我們需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過呼叫Thread的sleep方法。

Thread.sleep(long millis)方法,millis引數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。

2.wait(執行緒等待)

Object類中的wait()方法,導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 喚醒方法.

3.yield(執行緒讓步)

Thread.yield() 方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。

和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。

yield()方法只是讓當前執行緒暫停一下,重新進入就緒的執行緒池中,讓系統的執行緒排程器重新排程器重新排程一次,完全可能出現這樣的情況:當某個執行緒呼叫yield()方法之後,執行緒排程器又將其排程出來重新進入到執行狀態執行。

4.join(執行緒加入)

join()方法,等待其他執行緒終止。在當前執行緒中呼叫另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個程序執行結束,當前執行緒再由阻塞轉為就緒狀態。

5.notify(執行緒喚醒)

Object類中的notify()方法,喚醒在此物件監視器上等待的單個執行緒。如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過呼叫其中一個 wait 方法,在物件的監視器上等待。 直到當前的執行緒放棄此物件上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競爭

執行緒的優先順序

每一個 Java 執行緒都有一個優先順序,這樣有助於作業系統確定執行緒的排程順序。

Java 執行緒的優先順序是一個整數,其取值範圍是 1 (Thread.MINPRIORITY ) - 10 (Thread.MAXPRIORITY )。

預設情況下,每一個執行緒都會分配一個優先順序 NORM_PRIORITY(5)。

具有較高優先順序的執行緒對程式更重要,並且應該在低優先順序的執行緒之前分配處理器資源。但是,執行緒優先順序不能保證執行緒執行的順序,而且非常依賴於平臺。

執行緒池

執行緒池,其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。使用執行緒池的好處:

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗.
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

Java通過Executors提供四種執行緒池,分別為:

  1. newCachedThreadPoo 建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。
  2. newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
  3. newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
  4. newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。