天天看點

Java 多線程知識的簡單總結

Java 提供了多線程程式設計的内置支援,讓我們可以輕松開發多線程應用。

Java 中我們最為熟悉的線程就是 main 線程——主線程。

一個程序可以并發多個線程,每條線程并行執行不同的任務。線程是程序的基本機關,是一個單一順序的控制流,一個程序一直運作,直到所有的“非守護線程”都結束運作後才能結束。Java 中常見的守護線程有:垃圾回收線程、

這裡簡要述說以下并發和并行的差別。

并發:同一時間段内有多個任務在運作

并行:同一時間點上有多個任務同時在運作

多線程可以幫助我們高效地執行任務,合理利用 CPU 資源,充分地發揮多核 CPU 的性能。但是多線程也并不總是能夠讓程式高效運作的,多線程切換帶來的開銷、線程死鎖、線程異常等等問題,都會使得多線程開發較單線程開發更麻煩。是以,有必要學習 Java 多線程的相關知識,進而提高開發效率。

1 建立多線程

根據官方文檔 ​​Thread (Java Platform SE 8 ) (oracle.com)​​ 中 ​

​java.lang.Thread​

​ 的說明,可以看到線程的建立方式主要有兩種:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

可以看到,有兩種建立線程的方式:

  • 聲明一個類繼承​

    ​Thread​

    ​ 類,這個子類需要重寫​

    ​run​

    ​ 方法,随後建立這個子類的執行個體,這個執行個體就可以建立并啟動一個線程執行任務;
  • 聲明一個類實作接口​

    ​Runnable​

    ​ 并實作​

    ​run​

    ​ 方法。這個類的執行個體作為參數配置設定給一個​

    ​Thread​

    ​ 執行個體,随後使用​

    ​Thread​

    ​ 執行個體建立并啟動線程即可

除此之外的建立線程的方法,諸如使用 ​

​Callable​

​ 和 ​

​FutureTask​

​、線程池等等,無非是在此基礎上的擴充,檢視源碼可以看到 ​

​FutureTask​

​ 也實作了 ​

​Runnable​

​ 接口。

使用繼承 Thread 類的方法建立線程的代碼:

/**
 * 使用繼承 Thread 類的方法建立線程
 */
public class CreateOne {
    public static void main(String[] args) {
        Thread t = new MySubThread();
        t.start();
    }
}

class MySubThread extends Thread {
    @Override
    public void run() {
        // currentThread() 是 Thread 的靜态方法,可以擷取正在執行目前代碼的線程執行個體
        System.out.println(Thread.currentThread().getName() + "執行任務");
    }
}

// ================================== 運作結果
Thread-0執行任務      

使用實作 Runnable 接口的方法建立線程的代碼:

/**
 * 使用實作 Runnable 接口的方法建立線程
 */
public class CreateTwo {
    public static void main(String[] args) {
        RunnableImpl r = new RunnableImpl();
        Thread t = new Thread(r);
        t.start();
    }
}

class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "執行任務");
    }
}

// ================================== 運作結果
Thread-0執行任務      

1.1 孰優孰劣

建立線程雖然有兩種方法,但是在實際開發中,使用實作接口 ​

​Runnable​

​ 的方法更好,原因如下:

  1. 檢視​

    ​Thread​

    ​ 的​

    ​run​

    ​ 方法,可以看到:
// Thread 執行個體的成員變量 target 是一個 Runnable 執行個體,可以通過 Thread 的構造方法傳入
private Runnable target;

// 如果有傳入 Runnable 執行個體,那麼就執行它的 run 方法
// 如果重寫,就完全執行我們自己的邏輯
public void run() {
    if (target != null) {
        target.run();
    }
}      
  1. 檢視上面的源碼,我們可以知道,Thread 類并不是定義執行任務的主體,而是​

    ​Runnable​

    ​ 定義執行任務内容,​

    ​Thread​

    ​ 調用執行,進而實作線程與任務的解耦。
  2. 由于線程與任務解耦,我們可以複用線程,而不是當需要執行任務就去建立線程、執行完畢就銷毀線程,這樣帶來的系統開銷太大。這也是線程池的基本思想。
  3. 此外,Java 隻隻支援單繼承,如果繼承 Thread 使用多線程,那麼後續需要通過繼承的方式擴充功能,那會相當麻煩。

2 start 和 run 方法

從上面可以得知,有兩種建立線程的方式,我們通過 ​

​Thread​

​ 類或 ​

​Runnable​

​ 接口的 ​

​run​

​ 方法定義任務,通過 ​

​Thread​

​ 的 ​

​start​

​ 方法建立并啟動線程。

❗❗ 我們不能通過 ​

​run​

​ 方法啟動并建立一個線程,它隻是一個普通方法,如果直接調用這個方法,其實隻是調用這個方法的線程在執行任務罷了。

// 将上面的代碼修改一下,檢視執行結果
public class CreateOne {
    public static void main(String[] args) {
        Thread t = new MySubThread();
        t.run();
        //t.start();
    }
}

// ===================== 執行結果
main執行任務      

檢視 start 方法的源碼:

// 線程狀态,為 0 表示還未啟動
private volatile int threadStatus = 0;

// ❗❗ 同步方法,確定建立、啟動線程是線程安全的
public synchronized void start() {
    // 如果線程狀态不為 0,那麼抛出異常——💡即線程已經建立了
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // 将目前線程添加到線程組
    group.add(this);

    boolean started = false;
    try {
        // 這是一個本地方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

// 由本地方法實作,隻需要知道,該方法調用後會建立一個線程,并且會執行 run 方法
private native void start0();      

由上面的源碼可以得知:

  1. 建立并啟動一個線程是線程安全的
  2. start() 方法不能反複調用,否則會抛出異常

3 怎麼停止線程

線程并不是無休止地執行下去的,通常情況下,線程停止的條件有:

  1. run 方法執行結束
  2. 線程發生異常,但是沒有捕獲處理

除此之外,我們還需要自定義某些情況下需要通知線程停止,例如:

  1. 使用者主動取消任務
  2. 任務執行時間逾時、出錯
  3. 出現故障,服務需要快速停止
  4. ...

💡 為什麼不能直接簡單粗暴的停止線程呢?通過通知線程停止任務,我們可以更優雅地停止線程,讓線程儲存問題現場、記錄日志、發送警報、友好提示等等,令線程在合适的代碼位置停止線程,進而避免一些資料丢失等情況。

令線程停止的方法是讓線程捕獲中斷異常或檢測中斷标志位,進而優雅地停止線程,這是推薦的做法。而不推薦的做法有,使用被标記為過時的方法:​

​stop​

​,​

​resume​

​,​

​suspend​

​,這些方法可能會造成死鎖、線程不安全等情況,由于已經過時了,是以不做過多介紹。

3.1 通知線程中斷

我們要使用通知的方式停止目标線程,通過以下方法,希望能夠幫助你掌握中斷線程的方法:

/**
 * 中斷線程
 */
public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            long i = 0;
            // isInterrupted() 檢測目前線程是否處于中斷狀态
            while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println(i);
        });

        t.start();
        // 主線程睡眠 1 秒,通知線程中斷
        Thread.sleep(1000);
        t.interrupt();
    }
}
// 運作結果
1436125519      

這是中斷線程的方法之一,還有其他方法,當線程處于阻塞狀态時,線程并不能運作到檢測線程狀态的代碼位置,然後正确響應中斷,這個時候,我們需要通過捕獲異常的方式停止線程:

/**
 * 通過捕獲中斷異常停止線程
 */
public class InterruptThreadByException {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            long i = 0;
            while (i < Long.MAX_VALUE) {
                i++;
                try {
                    // 線程大部分時間處于阻塞狀态,sleep 方法會抛出中斷異常 InterruptedException
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // 捕獲到中斷異常,代表線程被通知中斷,做出相應處理再停止線程
                    System.out.println("線程收到中斷通知   " + i);
                    // 如果 try-catch 在 while 代碼塊之外,可以不用 return 也可以結束代碼
                    // 在 while 代碼塊之内,如果沒有 return / break,那麼還是會進入下一次循環,并不能正确停止
                    return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}
// 運作結果
線程收到中斷通知   10      

以上,就是停止線程的正确做法,此外,捕獲中斷異常後,會清除線程的中斷狀态,在實際開發中需要特别注意。例如,修改上面的代碼:

public class InterruptThreadByException {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            long i = 0;
            while (i < Long.MAX_VALUE) {
                i++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("線程收到中斷通知   " + i);
                    // ❗❗ 添加這行代碼,捕獲到中斷異常後,檢測中斷狀态,中斷狀态為 false
                    System.out.println(Thread.currentThread().isInterrupted());
                    return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}      

是以,線上程中,如果調用了其他方法,如果該方法有異常發生,那麼:

  1. 将異常抛出,而不是在子方法内部捕獲處理,由 run 方法統一處理異常
  2. 捕獲異常,并重新通知目前線程中斷,​

    ​Thread.currentThread().interrupt()​

例如:

public class SubMethodException {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ExceptionRunnableA());
        Thread t2 = new Thread(new ExceptionRunnableB());
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.interrupt();
        t2.interrupt();
    }
}

class ExceptionRunnableA implements Runnable {
    @Override
    public void run() {
        try {
            while (true) {
                method();
            }
        } catch (InterruptedException e) {
            System.out.println("run 方法内部捕獲中斷異常");
        }
    }

    public void method() throws InterruptedException {
        Thread.sleep(100000L);
    }
}

class ExceptionRunnableB implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            method();
        }
    }

    public void method()  {
        try {
            Thread.sleep(100000L);
        } catch (InterruptedException e) {
            System.out.println("子方法内部捕獲中斷異常");
            // 如果不重新設定中斷,線程将不能正确響應中斷
            Thread.currentThread().interrupt();
        }
    }
}      

綜上,總結出令線程正确停止的方法為:

  1. 使用​

    ​interrupt()​

    ​ 方法通知目标線程停止,标記目标線程的中斷狀态為​

    ​true​

  2. 目标線程通過​

    ​isInterrupted()​

    ​ 不時地檢測線程的中斷狀态,根據情況決定是否停止線程
  3. 如果線程使用了阻塞方法例如​

    ​sleep()​

    ​,那麼需要捕獲中斷異常并進行中斷通知,捕獲了中斷異常會重置中斷标記位
  4. 如果​

    ​run()​

    ​ 方法調用了其他子方法,那麼子方法:
  1. 将異常抛出,傳遞到頂層​

    ​run​

    ​ 方法,由​

    ​run​

    ​ 方法統一處理
  2. 将異常捕獲,同時重新通知目前線程中斷

下面再說說關于中斷的幾個相關方法和一些會抛出中斷異常的方法,使用的時候需要特别注意。

3.2 線程中斷的相關方法

  1. ​interrupt()​

    ​ 執行個體方法,通知目标線程中斷。
  2. ​static interrupted()​

    ​ 靜态方法,擷取目前線程是否處于中斷狀态,會重置中斷狀态,即如果中斷狀态為​

    ​true​

    ​,那麼調用後中斷狀态為​

    ​false​

    ​。方法内部通過​

    ​Thread.currentThread()​

    ​ 擷取執行線程執行個體。
  3. ​isInterrupted()​

    ​ 執行個體方法,擷取線程的中斷狀态,不會清除中斷狀态。

3.3 阻塞并能響應中斷的方法

  • ​Object.wait()​

  • ​Thread.sleep()​

  • ​Thread.join()​

  • ​BlockingQueue.take() / put()​

  • ​Lock.lockInterruptibly()​

  • ​CountDownLatch.await()​

  • ​CyclicBarrier.await()​

  • ​Exchanger.exchange()​

4 線程的生命周期

線程的生命周期狀态由六部分組成:

狀态 說明
NEW 線程剛建立,還沒有調用 ​

​start​

​ 方法,線程尚未啟動
RUNNABLE 線程已經調用了 ​

​start​

​ 方法,已經準備好運作,正在等待 CPU 配置設定資源;或者正在運作
BLOCKED 進入 ​

​synchronized​

​ 代碼塊,但是沒有拿到對象鎖,進入阻塞狀态
WAITING

​synchronized​

​​ 代碼塊中,同步鎖對象調用了 ​

​wait​

​​ 方法,或者線程被調用了 ​

​join​

​ 方法等,進入等待狀态,需要被喚醒
TIMED WAITING 計時等待狀态,等待一段時間自動蘇醒,或者等待過程中被喚醒
TERMINATED 線程執行結束,正常結束或者發生未捕獲的異常

可以用一張圖總結線程的生命周期,以及各個過程之間是如何轉換的:

Java 多線程知識的簡單總結

5 Thread 和 Object 中的線程方法

現在,我們已經知道了線程的建立、啟動、停止以及線程的生命周期了,那麼,再來看看線程相關的方法有哪些。

首先,看看 ​

​Thread​

​ 中的一些方法:

方法 說明
sleep() 讓線程等待一段時間,不會釋放鎖
join() 目前線程等待目标線程運作結束
yield() 放棄已經獲得的 CPU 資源,線程依舊是 RUNNABLE 狀态
currentThread() 擷取目前的線程執行個體
start() 啟動線程
run() 線程任務主體
interrupt() 通知線程中斷,設定中斷标志為 ​

​true​

isInterrupted() 檢查線程是否處于中斷狀态
interrupted() 傳回線程中斷狀态,會重置線程中斷狀态
stop()/suspend()/resume() 過時的停止線程方法

再看看 ​

​Object​

​ 中的相關方法:

方法 說明
wait() 線程擷取對象鎖,進入等待狀态,必須配合同步代碼塊使用;
要麼被喚醒,要麼計時等待時間結束
notify() 随機喚醒一個線程,被喚醒線程嘗試擷取同步鎖
notifyAll() 喚醒所有線程,所有線程都會嘗試擷取同步鎖

運作以下代碼,檢視 ​

​wait()​

​ 和 ​

​sleep()​

​ 是否會釋放同步鎖

/**
* 證明 sleep 不會釋放鎖,wait 會釋放鎖
*/
public class SleepAndWait {

    private static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "獲得同步鎖,調用 wait() 方法");
                try {
                    lock.wait(2000);
                    System.out.println(Thread.currentThread().getName() + "重新獲得同步鎖");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "獲得同步鎖,喚醒另一個線程,調用 sleep()");
                lock.notify();
                try {
                    // 如果 sleep() 會釋放鎖,那麼在此期間,上面的線程将會繼續運作,即 sleep 不會釋放同步鎖
                    Thread.sleep(2000);
                    // 如果執行 wait 方法,那麼上面的線程将會繼續執行,證明 wait 方法會釋放鎖
                    //lock.wait(2000);
                    System.out.println(Thread.currentThread().getName() + "sleep 結束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}      

上面的代碼已經證明了 ​

​sleep()​

​ 不會釋放同步鎖,此外,​

​sleep()​

​ 也不會釋放 ​

​Lock​

​ 的鎖,運作以下代碼檢視結果:

/**
 * sleep 不會釋放 Lock 鎖
 */
public class SleepDontReleaseLock implements Runnable {
    private static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 調用 lock 方法,線程會嘗試持有該鎖對象,如果已經被其他線程鎖住,那麼目前線程會進入阻塞狀态
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "獲得 lock 鎖");
            // 如果 sleep 會釋放 Lock 鎖,那麼另一個線程會馬上列印上面的語句
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "釋放 lock 鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 目前線程釋放鎖,讓其他線程可以占有鎖
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SleepDontReleaseLock task = new SleepDontReleaseLock();
        new Thread(task).start();
        new Thread(task).start();
    }
}      

5.1 wait 和 sleep 的異同

接下來總結 ​

​Object.wait()​

​ 和 ​

​Thread.sleep()​

​ 方法的異同點。

相同點:

  1. 都會使線程進入阻塞狀态
  2. 都可以響應中斷

不同點:

  1. ​wait()​

    ​ 是​

    ​Object​

    ​ 的執行個體方法,​

    ​sleep()​

    ​ 是​

    ​Thread​

    ​ 的靜态方法
  2. ​sleep()​

    ​ 需要指定時間
  3. ​wait()​

    ​ 會釋放鎖,​

    ​sleep()​

    ​ 不會釋放鎖,包括同步鎖和​

    ​Lock​

    ​ 鎖
  4. ​wait()​

    ​ 必須配合​

    ​synchronized​

    ​ 使用

6 線程的相關屬性

現在我們已經對 Java 中的多線程有一定的了解了,我們再看看 Java 中線程 ​

​Thread​

​ 的一些相關屬性,即它的成員變量。

屬性 說明
線程 ID 唯一辨別線程,無法修改,從 1 遞增,主線程 main ID 為 1
使用者建立的線程 ID 并不是從 2 開始,虛拟機程序啟動後還會建立其他線程,例如垃圾回收線程
名稱 name 預設為 ​

​Thread-自增ID​

可以通過 ​

​Thread.setName()​

​ 自定義線程名,友善區分線程、排查問題
是否是守護線程 daemon

​false​

​ 代表不為守護線程,一般會繼承父線程的類型
為目标線程提供服務,非守護線程運作結束後,會随着虛拟機一起停止
通常不使用守護線程,使用者線程一旦結束,守護線程也會結束
優先級 priority 優先級從小到大為 ​

​0-10​

​, 預設為 5
通常不改變優先級,因為:
1\. 不同作業系統的優先級定義不同
2\. 優先級會被作業系統修改
3\. 低優先級的線程可能一緻無法擷取資源而無法運作

運作以下代碼,了解線程的相關屬性

public class ThreadFields {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // 自定義線程的 ID 并不是從 2 開始
            System.out.println("線程 " + Thread.currentThread().getName()
                               + " 的線程 ID " + Thread.currentThread().getId());
            while (true) {
                // 守護線程一直運作,但是 使用者線程即這裡的主線程結束後,也會随着虛拟機一起停止
            }
        });
        // 自定義線程名字
        t.setName("自定義線程");
        // 将其設定為守護線程
        t.setDaemon(true);
        // 設定優先級 Thread.MIN_PRIORITY = 1     Thread.MAX_PRIORITY = 10
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
        // 主線程的 ID 為 1
        System.out.println("線程 " + Thread.currentThread().getName() + " 的線程 ID " + Thread.currentThread().getId());
        Thread.sleep(3000);
    }
}      

7 全局異常處理

在子線程中,如果發生了異常我們能夠及時捕獲并處理,那麼對程式運作并不會有什麼惡劣影響。

但是,如果發生了一些未捕獲的異常,在多線程情況下,這些異常列印出來的堆棧資訊,很容易淹沒在龐大的日志中,我們可能很難察覺到,并且不好排查問題。

如果對這些異常都做捕獲處理,那麼就會造成代碼的備援,編寫起來也不友善。

是以,我們可以編寫一個全局異常處理器來處理子線程中抛出的異常,統一地處理,解耦代碼。

7.1 源碼檢視

在講解如何處理子線程的異常問題前,我們先看看 JVM 預設情況下,是如何處理未捕獲的異常的。

檢視 Thread 的源碼:

public class Thread implements Runnable {
    【1】當發生未捕獲的異常時,JVM 會調用該方法,并傳遞異常資訊給異常處理器
        可以在這裡打下斷點,線上程中抛出異常不捕獲,IDEA 會跳轉到這裡
    // 向處理程式發送未捕獲的異常。此方法僅由JVM調用。
    private void dispatchUncaughtException(Throwable e) {
        【2】檢視第 9 行代碼,可以看到如果沒有指定異常處理器,預設是線程組作為異常處理器
        【3】調用這個異常處理器的處理方法,處理異常,檢視第 15 行
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

    【4】UncaughtExceptionHandler 是 Thread 的内部接口,線程組也是該接口的實作,
        隻有一個方法處理異常,接下來檢視第 25 行,看看 Group 是如何實作的
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }
}

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    【5】預設異常處理器的實作
    public void uncaughtException(Thread t, Throwable e) {
        // 如果有父線程組,交給它處理
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            // 擷取預設的異常處理器,如果沒有指定,那麼為 null
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } 
            // 沒有指定異常處理器,列印堆棧資訊
            else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
}      

7.2 自定義全局異常處理器

通過上面的源碼講解,已經可以知道 JVM 是如何處理未捕獲的異常的了,即隻列印堆棧資訊。那麼,要如何自定義異常處理器呢?

具體方法為:

  1. 實作接口​

    ​Thread.UncaughtExceptionHandler​

    ​ 并實作方法​

    ​uncaughtException()​

  2. 為建立的線程指定異常處理器

示例代碼:

public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("發生了未捕獲的異常,進行日志處理、報警處理、友好提示、資料備份等等......");
        e.printStackTrace();
    }

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            throw new RuntimeException();
        });
        t.setUncaughtExceptionHandler(new MyExceptionHandler());
        t.start();
    }
}      

8 多線程帶來的問題

合理地利用多線程能夠帶來性能上的提升,但是如果因為一些疏漏,多線程反而會成為程式員的噩夢。

例如,多線程開發,我們需要考慮線程安全問題、性能問題。

首先,講講線程安全問題。

什麼是線程安全?所謂線程安全,即

在多線程情況下,如果通路某個對象,不需要額外處理,例如加鎖、令線程阻塞、額外的線程排程等,調用這個對象都能獲得正确的結果,那麼這個對象就是線程安全的

是以,在編寫多線程程式時,就需要考慮某個資料是否是線程安全的,如果這個對象滿足:

  1. 被多個線程共享
  2. 操作具有時序要求,先讀後寫
  3. 這個對象的類有他人編寫,并且沒有聲明是線程安全的

那麼我們就需要考慮使用同步鎖、Lock、并發工具類(​

​java.util.concurrent​

​)來保證這個對象是在多線程下是安全的。

再看看多線程帶來的性能問題。

多個線程的排程需要上下文切換,這需要耗費 CPU 資源。

所謂上下文,即處理器中寄存器、程式計數器内的資訊。

上下文切換,即 CPU 挂起一個線程,将其上下文儲存到記憶體中,從記憶體中擷取另一個運作線程的上下文,恢複到寄存器中,根據程式計數器中的指令恢複線程運作。

一個線程被挂起,另一個線程恢複運作,這個時候,被挂起的線程的資料緩存對于運作線程來說是無效的,減緩了線程的運作速度,新的線程需要重新緩存資料提升運作速度。

9 總結

  1. 建立線程有兩種方式,繼承 Thread 和實作 Runnable
  2. start 方法才能正确建立和啟動線程,run 方法隻是一個普通方法
  3. start 方法不能反複調用,反複調用會抛出異常
  4. 正确停止線程的方法是通過​

    ​interrupt()​

    ​ 通知線程
  1. 線程不時地檢查中斷狀态并判斷是否停止線程,使用方法​

    ​isInterrupt()​

  2. 如果線程阻塞,捕獲中斷異常,判斷是否停止線程
  3. 線程調用的子方法最好将異常抛出,由 run 方法統一捕獲處理
  4. 線程調用的子方法如果捕獲異常,需要重新通知線程中斷
  1. 線程的生命周期為
  1. NEW
  2. RUNNABLE
  3. BLOCKED
  4. WAITING
  5. TIMED WAITING
  6. TERMINATED
  1. wait()/notify()/notifyAll() 必須配合同步鎖使用
  2. wait() 會釋放鎖,sleep() 不會釋放鎖,包括同步鎖和 Lock 鎖
  3. 線程的一些屬性
  1. 線程ID,無法修改
  2. 線程名 name,可以自定義
  3. 守護線程 daemon,線程類型會繼承自父線程,通常不指定線程為守護線程
  4. 優先級 priority,通常使用預設優先級,不改變優先級
  1. 可以自定義全局異常處理器,處理非主線程中的未捕獲的異常,如備份資料、日志處理、報警等等
  2. 多線程開發會帶來線程安全問題、性能問題,開發過程需要特别注意

繼續閱讀