線程基礎

1. 線程簡介

1.1 什么是線程

現代操作系統(tǒng)調度的最小單元是線程,也叫輕量級進程,在一個進程里可以創(chuàng)建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,并且能夠訪問共享的內存變量。

一個 Java 程序從 main() 方法開始執(zhí)行,然后按照既定的代碼邏輯執(zhí)行,看似沒有其他線程參與,但實際上 Java 程序天生就是多線程程序,因為執(zhí)行 main() 方法的是一個名稱為 main 的線程。下面演示了使用 JMX 來查看一個普通的 Java 程序包含哪些線程:

public class MultiThread {
    public static void main(String[] args) {
        // 獲取 Java 線程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要獲取同步 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍歷線程信息,僅打印線程 ID 和線程名稱
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
    }
}

輸出如下:

[5] Attach Listener    //添加事件
[4] Signal Dispatcher    // 分發(fā)處理給JVM信號的線程
[3] Finalizer    //調用對象finalize方法的線程
[2] Reference Handler    //清除reference線程
[1] main    //main線程,程序入口

1.2 為什么使用多線程

  1. 充分利用現代計算機的多核處理器
  2. 更快的響應時間
  3. 更好的編程模型

其實第三點應該就回答了為什么上面一個單線程簡單 Java 程序需要開這么多線程:多線程的引入,每個線程各司其職,最終這個程序運行的模型就簡單有條理。假設一下所有這些邏輯都糅雜在一個線程里實現,那這代碼該有多難看?

1.3 線程優(yōu)先級

現代操作系統(tǒng)基本采用時分的形式調度運行的線程,線程分配到多少時間片就決定了線程使用的處理器資源的多少,而線程優(yōu)先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。

但是,線程優(yōu)先級并不能作為程序正確的依賴,因為操作系統(tǒng)可以完全不理會 Java 線程對于優(yōu)先級的設定。

1.4 線程的狀態(tài)

Java 線程狀態(tài)的變遷

從圖中可以看到,Java 將操作系統(tǒng)中運行和就緒兩個狀態(tài)合并為運行狀態(tài) (RUNNNABLE) 。阻塞狀態(tài)是線程阻塞在進入 synchronized 關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態(tài),但是阻塞在 java.concurrent 包中 Lock 接口的線程狀態(tài)確是等待狀態(tài),因為 java.concurrent 包中的 Lock 接口對于阻塞的實現均使用了 LockSupport 類中的相關方法。

通過 jpsjstack 兩個命令我們可以查看到 Java 程序當前線程的狀態(tài)。

示例代碼

public class ThreadState {
    public static void main(String[] args) {
        new Thread(new TimeWaiting(), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        // 使用兩個 Blocked 線程,一個獲取鎖成功,另一個被阻塞
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }
    // 該線程不斷進行睡眠
    static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            while (true){
                SleepUtils.second(100);
            }
        }
    }

    // 該線程在 Waiting.class 實例上等待
    static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true){
                synchronized (Waiting.class){
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    // 該線程在 Blocked.class 實例上加鎖后,不會釋放該鎖
    static class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class){
                while (true){
                    SleepUtils.second(100);
                }
            }
        }
    }
}
public class SleepUtils {
    public static final void second(long seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

jstack 部分結果:

PS C:\Users\Administrator> jstack 18896
2018-12-15 15:40:36
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003382800 nid=0x46dc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"BlockedThread-2" #15 prio=5 os_prio=0 tid=0x000000001ef2b800 nid=0x18fc waiting for monitor entry [0x00000000206af000]
   java.lang.Thread.State: BLOCKED (on object monitor)

"BlockedThread-1" #14 prio=5 os_prio=0 tid=0x000000001ef27000 nid=0x4598 waiting on condition [0x00000000205ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

"WaitingThread" #13 prio=5 os_prio=0 tid=0x000000001ef26000 nid=0x18d4 in Object.wait() [0x00000000204af000]
   java.lang.Thread.State: WAITING (on object monitor)

"TimeWaitingThread" #12 prio=5 os_prio=0 tid=0x000000001ef24800 nid=0x1a60 waiting on condition [0x00000000203ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)

1.5 Daemon 線程

Daemon 線程翻譯成中文就是守護線程,用 jstack 命令可以看到一個普通 Java 程序都帶有守護線程,比如

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001ee0c800 nid=0x3214 runnable [0x000000001fcae000]

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001edcc000 nid=0x2d7c waiting on condition [0x0000000000000000]

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001ed7c000 nid=0x47e4 runnable [0x0000000000000000]

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001ed60800 nid=0x3618 in Object.wait() [0x000000001f23f000]

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000003478800 nid=0x3154 in Object.wait() [0x000000001ed3f000]

對于守護線程的使用,有一點要注意的是,不要依賴守護線程中的 finally 塊來關閉或者清理資源,因為只剩下守護線程時,程序會直接終止,而不會繼續(xù)執(zhí)行 finally 中的語句。

2. 啟動和終止線程

2.1 構造線程

線程不是憑空 new 出來的,它也需要要初始化 (init)。初始化線程是為線程提供所需要屬性,如線程所屬的線程組、線程優(yōu)先級、是否是 Deamon 線程等信息。初始化線程的過程中,子線程的空間分配參考父線程父線程,如子線程繼承了父線程是否為 Deamon、優(yōu)先級和加載資源的 contextClassLoader 以及可繼承的 ThreadLocal,同時還會分配一個唯一的 ID 來標識這個子線程。

2.2 啟動線程

啟動線程時,最好給線程設置一個有意義的名字,這樣通過 jstack 等工具排查問題時會給開發(fā)人員提供一些提示。

2.3 理解中斷

中斷是線程的一個標志位。其他線程調用某個線程的 interrupt() 方法就好比給該線程打了個招呼,希望該線程進行中斷。

需要被中斷的線程,通過判斷中斷標志來進行中斷響應,具體方式是調用 isInterrupted() 來進行判斷是否被中斷,也可以通過 interrupted() 方法來進行判斷中斷,不過這個方法的副作用是會復位中斷標志位。

另外,即使線程被中斷過,調用該線程對象的 isInterrupted() 方法仍會返回 false,因為從 Java API 中可以看到,許多聲明拋出 InterruptedException 的方法(例如 sleep),在拋出中斷異常前,會先復位中斷標志位。

運行下面的例子:

public class Interrupted {
    public static void main(String[] args) throws InterruptedException {
        // 不停嘗試睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "sleepThread");
        sleepThread.setDaemon(true);
        // busyThread 不停地運行
        Thread busyRunner = new Thread(new BusyRunner(), "busyRunner");
        busyRunner.setDaemon(true);
        sleepThread.start();
        busyRunner.start();
        // 休眠 5 秒,讓 sleepThread 和 busyThread 充分運行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyRunner.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyRunner.isInterrupted());
        // 防止 sleepThread 和 busyThread 立刻退出
        SleepUtils.second(2);
    }
    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true){
                SleepUtils.second(10);
            }

        }
    }

    static class BusyRunner implements Runnable {
        @Override
        public void run() {
            while (true){
            }
        }
    }
}

得到結果

java.lang.InterruptedException: sleep interrupted
SleepThread interrupted is false
    at java.lang.Thread.sleep(Native Method)
BusyThread interrupted is true
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at SleepUtils.second(SleepUtils.java:12)
    at Interrupted$SleepRunner.run(Interrupted.java:32)
    at java.lang.Thread.run(Thread.java:745)

可以看到,sleep 方法在拋出 InterruptedException 異常前,已經把中斷標記復位了。

2.4 過期的 suspend()、resume() 和 stop()

這三個方法都被標記為不建議使用,原因是不安全、不能保證釋放資源。

suspend() 方法在進入睡眠時,不會釋放已經占有的資源(比如鎖),這樣容易引發(fā)死鎖。stop() 在終結一個線程時,不會保證線程資源的正常釋放,導致程序容易陷入不確定的狀態(tài)。

對于 suspend-resume 可以使用 等待/通知 機制來替代。

2.5 安全地終止線程

可以模仿 interrupt 的方式,手動實現一個變量來控制是否需要停止任務并終止該線程。通過讓線程自己終止,而給線程充分的時間去銷毀、釋放資源。

3. 線程間通訊

3.1 volatile 和 synchronized 關鍵字

Java 支持多個線程同時訪問一個對象或者對象的成員變量,這些成員變量分配的內存是在共享內存中,但是每個線程可以擁有自己的一份獨立的拷貝,目的是加速程序的執(zhí)行,所以,在程序的執(zhí)行過程中,一個線程看到變量不一定是最新的

使用 volatile 或 synchronized 可以讓線程看到最新的變量,而不會讀取到舊值。具體實現方法是:

  • volatile

volatile 關鍵字的一個作用就是,類似于給成員變量加上了一個讀寫鎖,寫入新值,通知其他線程對該變量的緩存失效,達到刷新緩存的效果。

但是過多的使用 volatile 是不必要的,會降低程序的執(zhí)行效率。

  • synchronized

synchronized 有兩種使用方式

  1. 同步塊
  2. 修飾方法

它的實現類似于操作系統(tǒng)中的管程,保證同一時刻,只有一個線程處于方法或者同步塊中,從而保證了同一時刻只有一個線程在同步塊或方法中(臨界區(qū)),但是多了一個功能:其他線程拿到鎖后會刷新緩存,從而保證了線程對變量訪問的可見性和排他性。

下面通過一段簡單的代碼,用 javap 命令看看 synchronized 的在 .class 文件中是如何實現的。

public class Synchronized {
    public static void main(String[] args) {
        // 對 Synchronized Class 對象進行加鎖
        synchronized (Synchronized.class){
        }
        // 靜態(tài)同步方法,對 Synchronized Class 對象加鎖
        m();
    }
    public static synchronized void m(){
    }
}

執(zhí)行 javap -v 命令后,部分輸出如下:

 public static void main(java.lang.String[]);
         0: ldc           #2                  // class Synchronized
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
        15: invokestatic  #3                  // Method m:()V
        18: return

  public static synchronized void m();
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
}

可以看出,同步塊是使用 monitorenter 和 monitorexit 指令實現的,同步方法是通過方法上的修飾符 ACC_SYNCHRONIZED 完成的。這兩種方式本質都是進行排他性的獲取對象的監(jiān)視器。

3.2 等待/通知機制

等待/通知機制,是指一個線程 A 調用了對象 O 的 wait() 方法進入等待狀態(tài),而另一個線程調 B 調用了對象 O 的 notify() 或者 notifyAll() 方法,線程 A 收到通知后從對象 O 的 wait() 方法返回,進而執(zhí)行后續(xù)操作。

Java 內置的等待/通知的相關方法

方法名稱 描述
notify() 通知一個在對象上等待的線程,使其從 wait() 方法返回,而返回的前提是該線程獲取到了對象的鎖
notifyAll() 通知所有等待在該對象上的線程
wait() 調用該方法的線程進入 WAITING 狀態(tài),只有等待另外的線程的通知或者被中斷才會返回,需要注意,調用 wait() 方法后,會釋放對象的鎖
wait(long) 超時等待一段時間,這里的參數時間是毫秒,也就是長達 n 毫秒,如果沒有通知就超時返回
wait(long,int) 對于超時時間更細粒度的控制,可以達到納秒

一個樣例

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable {
        @Override
        public void run() {
            // 加鎖,擁有 lock 的 Monitor
            synchronized (lock) {
                // 當條件不滿足時,繼續(xù) wait,同時釋放了 lock 的鎖
                while (flag){
                    try {
                        System.out.println(Thread.currentThread() + " flag is true. wait @ " +
                                new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 條件滿足時,完成工作
                    System.out.println(Thread.currentThread() + " flag is false. running @ " +
                            new SimpleDateFormat("HH:mm:ss").format(new Date()));
                }
            }
        }
    }

    static class Notify implements Runnable {
        @Override
        public void run() {
            synchronized (lock){
                // 加鎖,擁有 lock 的Monitor
                // 直到當前線程釋放了 lock 后,WaitThread 才能從 wait 方法中返回
                System.out.println(Thread.currentThread() + " hold lock. notify @ " +
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 再次加鎖
            synchronized (lock){
                System.out.println(Thread.currentThread() + " hold lock again. sleep @ " +
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

輸出如下:

Thread[WaitThread,5,main] flag is true. wait @ 10:48:59
Thread[NotifyThread,5,main] hold lock. notify @ 10:49:00
Thread[WaitThread,5,main] flag is false. running @ 10:49:05
Thread[NotifyThread,5,main] hold lock again. sleep @ 10:49:05

可以看到, wait() 方法會釋放線程持有的對象鎖。從 wait() 方法返回后,并不具有獲取鎖的更高優(yōu)先級,仍是跟其他線程一起公平競爭對象的鎖。

3.3 等待/通知的經典范式

范式分為兩部分,分別針對等待方(消費者) 和通知方 (生產者)。

等待方遵循如下原則:

  1. 獲取對象的鎖。
  2. 如果條件不滿足,那么調用對象的 wait() 方法,被通知后仍要檢查條件。
  3. 條件滿足則執(zhí)行對應的邏輯。

對應的偽代碼如下:

synchronized(對象){
  while (條件不滿足){
    對象.wait();
  }
  對應的處理邏輯
}

通知方遵循如下原則

  1. 獲得對象的鎖
  2. 改變條件
  3. 通知所有等待在對象上的線程。

對應的偽代碼如下:

synchronized (對象){
  改變條件
  對象.notifyAll();
}

3.4 Thread.join() 的使用

如果一個線程 A 執(zhí)行了 Thread.join() 語句,其含義是:當前線程 A 等待 thread 線程終止后才從 thread.join() 返回。

Thread.join() 的返回涉及到等待/通知機制。下面是 JDK 中的 Thread.join() 方法的源碼(進行了部分調整)

// 加鎖當前線程對象
public final synchronized void join() throws InterruptedException {
  // 條件不滿足,繼續(xù)等待
  while (isAlive()){
    wait(0);
  }
  // 條件符合,方法返回
}

當線程終止時,會調用線程自身的 notifyAll() 方法,會通知所有等待在該線程對象上的線程??梢钥吹?join() 方法的邏輯結構于 3.3 中的等待/通知經典范式一致,即加鎖、循環(huán)和處理邏輯 3 個步驟。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,946評論 12 45
  • 一、線程概念 1. 操作系統(tǒng)中的線程現在的操作系統(tǒng)是多任務操作系統(tǒng),多線程是實現多任務的一種方式,在操作系統(tǒng)中,每...
    TyiMan閱讀 1,998評論 1 35
  • 1. 場景困惑: 在主線程中開啟一個線程t1 , 那么我如何能夠獲取這個線程t1的執(zhí)行狀態(tài):是否開始執(zhí)行?是...
    _Danniel_閱讀 376評論 0 0
  • 一、線程基本概念 1. 線程的五種狀態(tài) 新建狀態(tài)(new): 線程對象被創(chuàng)建后,就進入了新建狀態(tài)。例如,Threa...
    Lynn_R01612x2閱讀 474評論 0 1
  • 簡介 本次主要介紹java多線程中的同步,也就是如何在java語言中寫出線程安全的程序。如何在java語言中解決非...
    小人物灌籃閱讀 536評論 0 1

友情鏈接更多精彩內容