程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java筆記 - 線程基礎知識

Java筆記 - 線程基礎知識

編輯:JAVA綜合教程

Java筆記 - 線程基礎知識


前言

進程是一個執行中程序的實例,是操作系統進行資源分配和調度的一個獨立單元。線程是進程中一個單一的程序控制流,是 CPU 調度和分派的基本單元。進程在執行時擁有獨立的內存空間,進程中的線程可以共享進程的內存空間。在 Java 的世界中,進程可以擁有多個並發執行的線程,多線程是實現並發任務的方式。

線程創建和啟動

1. 實現 java.lang.Runnable 接口
定義線程執行的任務,需要實現 Runnable 接口並編寫 run 方法。

public interface Runnable {

    /**
     * Starts executing the active part of the class' code. This method is
     * called when a thread is started that has been created with a class which
     * implements {@code Runnable}.
     */
    public void run();
}

以下示例通過實現 Runnable 接口來模擬火箭發射之前倒計時的任務:

public class LiftOff implements Runnable {
    private int countDown;

    public LiftOff(int countDown) {
        this.countDown = countDown;
    }

    private String status() {
        return countDown > 0 ? String.valueOf(countDown) : "LiftOff!";
    }

    @Override
    public void run() {
        while (countDown-- > 0) {
            System.out.println(status());
        }
    }
}

2. 實現 java.util.concurrent.Callable 接口
Callable 接口的 call 方法可以在任務執行結束後產生一個返回值,以下是 Callable 接口的定義:

public interface Callable {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以使用 ExecutorService 類的 submit 方法提交實現了 Callable 接口的任務,submit 方法會產生一個 Future 對象,它用 call 方法返回結果的類型進行了參數化。可以通過 isDone 方法來查詢 Future 是否已經完成,如果已完成,則可以調用 get 方法獲取結果。也可以不使用 isDone 方法進行檢測就直接調用 get 方法獲取結果,此時如果結果還未准備就緒,get 方法將阻塞直到結果准備就緒。

class TaskWithResult implements Callable {
    private int id;

    public TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "Result of TaskWithResult " + id;
    }
}

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        // save Future object of submitted task
        List> futureList = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            futureList.add(exec.submit(new TaskWithResult(i)));
        }

        // waiting for all results
        while (futureList.size() > 0) {
            for (Future future : futureList) {
                if (future.isDone()) {
                    try {
                        System.out.println(future.get());
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    } finally {
                        futureList.remove(future);
                    }
                }
            }
        }

        exec.shutdown();
    }
}

3. 繼承 java.lang.Thread 類
Thread 類的構造方法包含一個 Runnable 任務,以下是 Thread 類的構造方法:

public Thread(Runnable runnable);
public Thread(Runnable runnable, String threadName);
public Thread(ThreadGroup group, Runnable runnable);
public Thread(ThreadGroup group, Runnable runnable, String threadName);
public Thread(ThreadGroup group, Runnable runnable, String threadName, long stackSize);

調用 Thread 類的 start 方法啟動線程,其 start 方法為新線程執行必要的初始化,然後調用 Runnable 的 run 方法,從而在新線程中啟動該任務,當 run 方法執行結束時該線程終止。

public class BasicThreads {
    public static void main(String[] args) {
        Thread thread = new Thread(new LiftOff(10));
        thread.start();
    }
}

綜上,Runnable 和 Callable 是工作單元,而 Thread 即充當工作單元,又是執行機制。一般而言,Runnable 和 Callable 優先於 Thread,因為可以獲得更大的靈活性。

線程狀態

在任意一個時間點,線程只可以處於以下六種狀態之一:

    public enum State {
        /**
         * The thread has been created, but has never been started.
         */
        NEW,
        /**
         * The thread may be run.
         */
        RUNNABLE,
        /**
         * The thread is blocked and waiting for a lock.
         */
        BLOCKED,
        /**
         * The thread is waiting.
         */
        WAITING,
        /**
         * The thread is waiting for a specified amount of time.
         */
        TIMED_WAITING,
        /**
         * The thread has been terminated.
         */
        TERMINATED
    }

1. 新建(new)狀態
線程此時已經分配了必要的系統資源,並執行了初始化,之後線程調度器將線程轉變為可運行狀態或者阻塞狀態。
2. 可運行(Runnable)狀態
線程此時正在運行或者等待線程調度器把時間片分配給它。
3. 阻塞(Blocked)狀態
線程此時由於需要獲取某個排他鎖而阻塞,線程調度器將不會分配時間片給該線程,直到線程滿足條件後進入可運行狀態。
4. 等待(Waiting)狀態
線程此時不會被分配時間片,要等待被其他線程顯式地喚醒。
線程從可運行狀態進入等待狀態,可能有如下原因:
[1] 調用沒有設置 Timeout 參數的 Object.wait 方法。
[2] 調用沒有設置 Timeout 參數的 Thread.join 方法。
[3] 線程在等待某個 I/O 操作完成。
5. 限期等待(Timed_waiting)狀態
線程此時不會被分配時間片,如果限期時間到期後還沒有被其他線程顯式地喚醒,則由系統自動喚醒。
線程從可運行狀態進入限期等待狀態,可能有如下原因:
[1] 調用 Thread.sleep 方法。
[2] 調用設置了 Timeout 參數的 Object.wait 方法。
[3] 調用設置了 Timeout 參數的 Thread.join 方法。
6. 終止(Terminated)狀態
線程此時不再是可調度的。線程終止通常方式是從 run 方法返回,或者線程被中斷。

線程各種狀態之間的關系如下圖所示:
這裡寫圖片描述
線程的當前狀態可以通過 getState 方法獲得,getState 方法定義如下所示:

    /**
     * Returns the current state of the Thread. This method is useful for
     * monitoring purposes.
     *
     * @return a {@link State} value.
     */
    public State getState() {
        return State.values()[nativeGetStatus(hasBeenStarted)];
    }

線程調度是指系統為線程分配 CPU 使用權的過程,主要調度方式有兩種,分別是協同式線程調度和搶占式線程調度。
使用協同式調度的多線程系統,線程的執行時間由線程本身來控制。協同式多線程的優點:實現簡單,而且線程完成任務後才會請求進行線程切換,切換操作對線程而言是可知的,所以沒有太多線程同步問題;它的缺點也很明顯:線程執行時間不可控,如果某個線程編寫有問題,會導致整個進程阻塞。
搶占式多線程每個線程將由系統來分配執行時間,線程切換不是由線程本身來決定的(在 Java 中,Thread.yield 方法可以出讓 CPU 使用權,但沒有辦法繞過線程調度器獲得 CPU 使用權),所以執行時間是系統可控的,不會出現某個線程導致整個進程阻塞的問題。目前 Java 使用的線程調度方式是搶占式線程調度。

線程常用方法

1. sleep 方法
sleep 方法會讓當前運行線程暫停執行指定的時間,將 CPU 讓給其他線程使用,但線程仍然保持對象的鎖,因此休眠結束後線程會回到可運行狀態。

public static void sleep(long time) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;

2. join 方法
如果線程 A 調用目標線程 B 的 join 方法,則線程 A 將會被掛起,直到線程 B 結束才恢復。或者在調用 join 方法時傳入超時參數,如果目標線程在這段時間到期還沒有結束的話,join 方法也會返回。

public final void join() throws InterruptedException;
public final void join(long millis) throws InterruptedException;
public final void join(long millis, int nanos) throws InterruptedException;

3. yield 方法
yield 方法讓當前運行線程回到可運行狀態,使得優先級相同或者更高的線程有機會被調度執行。和 sleep 方法一樣,線程仍然保持對象的鎖,因此調用 yield 方法後也會回到可運行狀態。

public static native void yield();

4. interrupt 方法
interrupt 方法將給線程的發送中斷請求,行為取決於線程當時的狀態。如果線程已經被阻塞,或者試圖執行一個阻塞操作,那麼設置這個線程的中斷狀態將會拋出 InterruptedException。當拋出 InterruptedException 異常或者調用 Thread.interrupted 方法時,中斷狀態將被清除,這樣確保不會在某個線程被中斷時出現兩次通知。

public void interrupt();

在線程上調用 interrupt 方法時,中斷發生的唯一時刻是線程要進入到阻塞操作,或者已經在阻塞操作過程中。其中 sleep/wait/join/NIO 阻塞是可中斷的,I/O 阻塞和 synchronized 同步阻塞是不可中斷的。

後台線程

後台(daemon)線程是指程序運行時在後台提供服務的線程。後台線程不屬於程序中不可或缺的部分,當所有的非後台線程結束時,進程會終止,同時會結束進程中所有的後台進程。
在線程啟動之前調用 setDaemon 方法,才能把線程設置為後台進程。通過使用 isDaemon 方法可以測試線程是否屬於後台線程,後台線程創建的任何線程將被自動設置成為後台線程。示例如下所示:

public class SimpleDaemons implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            // must call before start
            daemon.setDaemon(true);
            daemon.start();
            System.out.println("Daemon or not: " + (daemon.isDaemon() ? " yes" : "not"));
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(200);
    }
}

後台線程也比較常見,比如 Zygote 就有四個後台線程,分別是HeapTaskDaemon(堆整理線程), ReferenceQueueDaemon(引用隊列線程), FinalizerDaemon(終結方法線程), FinalizerWatchdogDaemon(終結方法監控線程) 。

線程之間的協作

當多個線程一起工作完成某些任務時,線程彼此之間需要協作。線程之間的協作關鍵問題在於握手,這種握手可以通過 Object 對象的 wait 和 notify/notifyAll 方法來實現,也可以通過 Java SE5 提供的具有 await 和 signal 方法的 Condition 對象來實現。

1. wait 與 notify/notifyAll 方法
wait 方法使線程等待某個條件發現變化,通常這個條件由另一個線程來改變。wait 方法被調用時,當前線程進入等待狀態,對象上的鎖會被釋放,因此該對象中其他 synchronized 方法可以在 wait 期間被調用。
使用 notify 方法可以喚醒一個調用 wait 方法進入等待狀態的線程,使用 notifyAll 方法可以喚醒所有調用 wait 方法進入等待狀態的線程。由於可能有多個線程在單個對象上處於 wait 狀態,因此使用 notifyAll 比調用 notify 更安全。使用 notify 而不是 notifyAll 是一種優化,在多個等待同一個條件的線程中只有一個會被喚醒,因此如果使用 notify,必須確保被喚醒的是恰當的線程。
借用 Java 編程思想中的一個示例,汽車保養時打蠟(wax)和拋光(buff)需要協調進行,先打完一層蠟,然後進行拋光,繼續打下一層蠟,然後繼續拋光,依次循環。以下示例通過 wait 與 notifyAll 方法來完成線程之間的協作:

class Car {
    private boolean waxOn = false;
    public synchronized void waxed() {
        // ready to buff
        waxOn = true;
        notifyAll();
    }

    public synchronized void buffed() {
        // ready to wax
        waxOn = false;
        notifyAll();
    }

    public synchronized void waitForWaxing() throws InterruptedException {
        while (!waxOn)
            wait();
    }

    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn)
            wait();
    }
}

class WaxTask implements Runnable {
    private Car car;
    public WaxTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("Wax On!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax On task");
    }
}

class BuffTask implements Runnable {
    private Car car;
    public BuffTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax Off task");
    }
}

public class WaxBuff {
    public static void main(String[] args) throws Exception {
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new WaxTask(car));
        exec.execute(new BuffTask(car));

        TimeUnit.SECONDS.sleep(3);
        // interrupt all submitted tasks
        exec.shutdownNow();
    }
}

wait, notify 和 notifyAll 方法屬於基類 Object 中的方法,它們只能在同步方法或者同步控制塊裡調用,否則程序運行時將拋出 IllegalMonitorStateException 異常。

2. Lock 與 Condition 對象
Java SE5 的 java.util.concurrent 類庫提供的 Condition 類也可以用於線程間的協作。通過 Condition 對象的 await 方法來等待某個條件發現變化。當外部條件發生變化時,通過調用 signal 方法來喚醒被掛起的線程,或者通過調用 signalAll 方法來喚醒所有在這個 Condition 上被掛起的線程。
以下使用 ReentrantLock 替換 synchronized 方法,使用 Condition 類提供的 await 和 signalAll 方法對汽車保養過程進行改寫:

class Car {
    private boolean waxOn = false;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void waxed() {
        lock.lock();
        try {
            // ready to buff
            waxOn = true;
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void buffed() {
        lock.lock();
        try {
            // ready to wax
            waxOn = false;
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void waitForWaxing() throws InterruptedException {
        lock.lock();
        try {
            while (!waxOn) {
                condition.await();
            }
        } finally {
            lock.unlock();
        }
    }

    public void waitForBuffing() throws InterruptedException {
        lock.lock();
        try {
            while (waxOn)
                condition.await();
        } finally {
            lock.unlock();
        }
    }
}

class WaxTask implements Runnable {
    private Car car;
    public WaxTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("Wax On!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax On task");
    }
}

class BuffTask implements Runnable {
    private Car car;
    public BuffTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax Off task");
    }
}

public class WaxBuff {
    public static void main(String[] args) throws Exception {
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new BuffTask(car));
        exec.execute(new WaxTask(car));

        TimeUnit.SECONDS.sleep(3);
        // interrupt all submitted tasks
        exec.shutdownNow();
    }
}

明顯感覺正確使用以上線程協作方式還是比較困難的,所以應該使用更高級的工具比如說同步器(Synchronizer)來代替。同步器是一些可以使線程等待另一個線程的對象,允許它們之間協調推進,常用的同步器包括 CountDownLatch, Semaphore, CyclicBarrier 等。

後記

Java 線程看上去簡單,但實際使用過程中有很多值得注意的地方,稍有不慎就可能碰到奇怪的問題,所以使用線程時需要非常仔細甚至保守。以上是我認為關於線程值得整理的內容,更多相關內容可以查閱參考資料。

參考資料

1. Java 編程思想(第 4 版)
2. 深入理解 Java 虛擬機(第 2 版)
3. Effective Java(第 2 版)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved