萬字圖解Java多線程,不信你學不會

2020-09-21 舒霞

前言

java多線程我個人覺得是javaSe中最難的一部分,我以前也是感覺學會了,但是真正有多線程的需求卻不知道怎麼下手,實際上還是對多線程這塊知識了解不深刻,不知道多線程api的應用場景,不知道多線程的運行流程等等,本篇文章將使用實例+圖解+源碼的方式來解析java多線程。

文章篇幅較長,大家也可以有選擇的看具體章節,建議多線程的代碼全部手敲,永遠不要相信你看到的結論,自己編碼後運行出來的,才是自己的。

什麼是java多線程?

進程與線程

進程

  • 當一個程序被運行,就開啟了一個進程, 比如啟動了qq,word
  • 程序由指令和數據組成,指令要運行,數據要加載,指令被cpu加載運行,數據被加載到內存,指令運行時可由cpu調度硬碟、網絡等設備

線程

  • 一個進程內可分為多個線程
  • 一個線程就是一個指令流,cpu調度的最小單位,由cpu一條一條執行指令

並行與並發

並發:單核cpu運行多線程時,時間片進行很快的切換。線程輪流執行cpu

並行:多核cpu運行 多線程時,真正的在同一時刻運行

java提供了豐富的api來支持多線程。

為什麼用多線程?

多線程能實現的都可以用單線程來完成,那單線程運行的好好的,為什麼java要引入多線程的概念呢?

多線程的好處:

  1. 程序運行的更快!快!快!
  2. 充分利用cpu資源,目前幾乎沒有線上的cpu是單核的,發揮多核cpu強大的能力

多線程難在哪裡?

單線程只有一條執行線,過程容易理解,可以在大腦中清晰的勾勒出代碼的執行流程

多線程卻是多條線,而且一般多條線之間有交互,多條線之間需要通信,一般難點有以下幾點

  1. 多線程的執行結果不確定,受到cpu調度的影響
  2. 多線程的安全問題
  3. 線程資源寶貴,依賴線程池操作線程,線程池的參數設置問題
  4. 多線程執行是動態的,同時的,難以追蹤過程
  5. 多線程的底層是作業系統層面的,源碼難度大

有時候希望自己變成一個字節穿梭於伺服器中,搞清楚來龍去脈,就像無敵破壞王一樣(沒看過這部電影的可以看下,腦洞大開)。

java多線程的基本使用

定義任務、創建和運行線程

任務:線程的執行體。也就是我們的核心代碼邏輯

定義任務

  1. 繼承Thread類 (可以說是 將任務和線程合併在一起)
  2. 實現Runnable接口 (可以說是 將任務和線程分開了)
  3. 實現Callable接口 (利用FutureTask執行任務)

Thread實現任務的局限性

  1. 任務邏輯寫在Thread類的run方法中,有單繼承的局限性
  2. 創建多線程時,每個任務有成員變量時不共享,必須加static才能做到共享

Runnable和Callable解決了Thread的局限性

但是Runbale相比Callable有以下的局限性

  1. 任務沒有返回值
  2. 任務無法拋異常給調用方

如下代碼 幾種定義線程的方式

@Slf4jclass T extends Thread {    @Override    public void run() {        log.info(&34;);    }}@Slf4jclass R implements Runnable {    @Override    public void run() {        log.info(&34;);    }}@Slf4jclass C implements Callable<String> {    @Override    public String call() throws Exception {        log.info(&34;);        return &34;;    }}

創建線程的方式

  1. 通過Thread類直接創建線程
  2. 利用線程池內部創建線程

啟動線程的方式

  • 調用線程的start()方法

// 啟動繼承Thread類的任務new T().start();// 啟動繼承Thread匿名內部類的任務 可用lambda優化Thread t = new Thread(){  @Override  public void run() {    log.info(&34;);  }};//  啟動實現Runnable接口的任務new Thread(new R()).start();//  啟動實現Runnable匿名實現類的任務new Thread(new Runnable() {    @Override    public void run() {        log.info(&34;);    }}).start();//  啟動實現Runnable的lambda簡化後的任務new Thread(() -> log.info(&34;)).start();// 啟動實現了Callable接口的任務 結合FutureTask 可以獲取線程執行的結果FutureTask<String> target = new FutureTask<>(new C());new Thread(target).start();log.info(target.get());

以上各個線程相關的類的類圖如下

上下文切換

多核cpu下,多線程是並行工作的,如果線程數多,單個核又會並發的調度線程,運行時會有上下文切換的概念

cpu執行線程的任務時,會為線程分配時間片,以下幾種情況會發生上下文切換。

  1. 線程的cpu時間片用完
  2. 垃圾回收
  3. 線程自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法

當發生上下文切換時,作業系統會保存當前線程的狀態,並恢復另一個線程的狀態,jvm中有塊內存地址叫程序計數器,用於記錄線程執行到哪一行代碼,是線程私有的。

idea打斷點的時候可以設置為Thread模式,idea的debug模式可以看出棧幀的變化

線程的禮讓-yield()&線程的優先級

yield()方法會讓運行中的線程切換到就緒狀態,重新爭搶cpu的時間片,爭搶時是否獲取到時間片看cpu的分配。

代碼如下

// 方法的定義public static native void yield();Runnable r1 = () -> {    int count = 0;    for (;;){       log.info(&34; + count++);    }};Runnable r2 = () -> {    int count = 0;    for (;;){        Thread.yield();        log.info(&34; + count++);    }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.start();t2.start();// 運行結果11:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950411:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950511:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950611:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950711:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950811:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950911:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951011:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951111:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951211:49:15.798 [t2] INFO thread.TestYield -             ---- 2>29311:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951311:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951411:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951511:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951611:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951711:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

如上述結果所示,t2線程每次執行時進行了yield(),線程1執行的機會明顯比線程2要多。

線程的優先級

線程內部用1~10的數來調整線程的優先級,默認的線程優先級為NORM_PRIORITY:5

cpu比較忙時,優先級高的線程獲取更多的時間片

cpu比較閒時,優先級設置基本沒用

 public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;  // 方法的定義 public final void setPriority(int newPriority) { }

cpu比較忙時

Runnable r1 = () -> {    int count = 0;    for (;;){       log.info(&34; + count++);    }};Runnable r2 = () -> {    int count = 0;    for (;;){        log.info(&34; + count++);    }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.setPriority(Thread.NORM_PRIORITY);t2.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();// 可能的運行結果11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>4410211:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>13590311:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>13590411:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>13590511:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906

cpu比較閒時

Runnable r1 = () -> {    int count = 0;    for (int i = 0; i < 10; i++) {        log.info(&34; + count++);    }};Runnable r2 = () -> {    int count = 0;    for (int i = 0; i < 10; i++) {        log.info(&34; + count++);    }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.setPriority(Thread.MIN_PRIORITY);t2.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();// 可能的運行結果 線程1優先級低 卻先運行完12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>712:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>812:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>912:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>212:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>312:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>412:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>512:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>612:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>712:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>812:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>9

守護線程

默認情況下,java進程需要等待所有線程都運行結束,才會結束,有一種特殊線程叫守護線程,當所有的非守護線程都結束後,即使它沒有執行完,也會強制結束。

默認的線程都是非守護線程。

垃圾回收線程就是典型的守護線程

// 方法的定義public final void setDaemon(boolean on) {}Thread thread = new Thread(() -> {    while (true) {    }});// 具體的api。設為true表示未守護線程,當主線程結束後,守護線程也結束。// 默認是false,當主線程結束後,thread繼續運行,程序不停止thread.setDaemon(true);thread.start();log.info(&34;);

線程的阻塞

線程的阻塞可以分為好多種,從作業系統層面和java層面阻塞的定義可能不同,但是廣義上使得線程阻塞的方式有下面幾種

  1. BIO阻塞,即使用了阻塞式的io流
  2. sleep(long time) 讓線程休眠進入阻塞狀態
  3. a.join() 調用該方法的線程進入阻塞,等待a線程執行完恢復運行
  4. sychronized或ReentrantLock 造成線程未獲得鎖進入阻塞狀態 (同步鎖章節細說)
  5. 獲得鎖之後調用wait()方法 也會讓線程進入阻塞狀態 (同步鎖章節細說)
  6. LockSupport.park() 讓線程進入阻塞狀態 (同步鎖章節細說)

sleep()

使線程休眠,會將運行中的線程進入阻塞狀態。當休眠時間結束後,重新爭搶cpu的時間片繼續運行

// 方法的定義 native方法public static native void sleep(long millis) throws InterruptedException; try {   // 休眠2秒   // 該方法會拋出 InterruptedException異常 即休眠過程中可被中斷,被中斷後拋出異常   Thread.sleep(2000); } catch (InterruptedException異常 e) { } try {   // 使用TimeUnit的api可替代 Thread.sleep    TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }

join()

join是指調用該方法的線程進入阻塞狀態,等待某線程執行完成後恢復運行

// 方法的定義 有重載// 等待線程執行完才恢復運行public final void join() throws InterruptedException {}// 指定join的時間。指定時間內 線程還未執行完 調用方線程不繼續等待就恢復運行public final synchronized void join(long millis)    throws InterruptedException{}

Thread t = new Thread(() -> {    try {        Thread.sleep(1000);    } catch (InterruptedException e) {        e.printStackTrace();    }    r = 10;});t.start();// 讓主線程阻塞 等待t線程執行完才繼續執行 // 去除該行,執行結果為0,加上該行 執行結果為10t.join();log.info(&34;, r);// 運行結果13:09:13.892 [main] INFO thread.TestJoin - r:10

線程的打斷-interrupt()

// 相關方法的定義public void interrupt() {}public boolean isInterrupted() {}public static boolean interrupted() {}

打斷標記:線程是否被打斷,true表示被打斷了,false表示沒有

isInterrupted() 獲取線程的打斷標記 ,調用後不會修改線程的打斷標記

interrupt()方法用於中斷線程

  1. 可以打斷sleep,wait,join等顯式的拋出InterruptedException方法的線程,但是打斷後,線程的打斷標記還是false
  2. 打斷正常線程 ,線程不會真正被中斷,但是線程的打斷標記為true

interrupted() 獲取線程的打斷標記,調用後清空打斷標記 即如果獲取為true 調用後打斷標記為false (不常用)

interrupt實例:有個後臺監控線程不停的監控,當外界打斷它時,就結束運行。代碼如下

@Slf4jclass TwoPhaseTerminal{    // 監控線程    private Thread monitor;    public void start(){        monitor = new Thread(() ->{           // 不停的監控            while (true){                Thread thread = Thread.currentThread();              // 判斷當前線程是否被打斷                if (thread.isInterrupted()){                    log.info(&34;);                    break;                }                try {                    Thread.sleep(1000);                 // 監控邏輯中被打斷後,打斷標記為true                    log.info(&34;);                } catch (InterruptedException e) {                    // 睡眠時被打斷時拋出異常 在該處捕獲到 此時打斷標記還是false                    // 在調用一次中斷 使得中斷標記為true                    thread.interrupt();                }            }        });        monitor.start();    }    public void stop(){        monitor.interrupt();    }}

線程的狀態

上面說了一些基本的api的使用,調用上面的方法後都會使得線程有對應的狀態。

線程的狀態可從 作業系統層面分為五種狀態 從java api層面分為六種狀態。

五種狀態

  1. 初始狀態:創建線程對象時的狀態
  2. 可運行狀態(就緒狀態):調用start()方法後進入就緒狀態,也就是準備好被cpu調度執行
  3. 運行狀態:線程獲取到cpu的時間片,執行run()方法的邏輯
  4. 阻塞狀態: 線程被阻塞,放棄cpu的時間片,等待解除阻塞重新回到就緒狀態爭搶時間片
  5. 終止狀態: 線程執行完成或拋出異常後的狀態

六種狀態

Thread類中的內部枚舉State

public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;}

  1. NEW 線程對象被創建
  2. Runnable 線程調用了start()方法後進入該狀態,該狀態包含了三種情況
    1. 就緒狀態 :等待cpu分配時間片
    2. 運行狀態:進入Runnable方法執行任務
    3. 阻塞狀態:BIO 執行阻塞式io流時的狀態
  3. Blocked 沒獲取到鎖時的阻塞狀態(同步鎖章節會細說)
  4. WAITING 調用wait()、join()等方法後的狀態
  5. TIMED_WAITING 調用 sleep(time)、wait(time)、join(time)等方法後的狀態
  6. TERMINATED 線程執行完成或拋出異常後的狀態

六種線程狀態和方法的對應關係

線程的相關方法總結

主要總結Thread類中的核心方法




方法名稱是否static方法說明start()否讓線程啟動,進入就緒狀態,等待cpu分配時間片run()否重寫Runnable接口的方法,線程獲取到cpu時間片時執行的具體邏輯yield()是線程的禮讓,使得獲取到cpu時間片的線程進入就緒狀態,重新爭搶時間片sleep(time)是線程休眠固定時間,進入阻塞狀態,休眠時間完成後重新爭搶時間片,休眠可被打斷join()/join(time)否調用線程對象的join方法,調用者線程進入阻塞,等待線程對象執行完或者到達指定時間才恢復,重新爭搶時間片isInterrupted()否獲取線程的打斷標記,true:被打斷,false:沒有被打斷。調用後不會修改打斷標記interrupt()否打斷線程,拋出InterruptedException異常的方法均可被打斷,但是打斷後不會修改打斷標記,正常執行的線程被打斷後會修改打斷標記interrupted()否獲取線程的打斷標記。調用後會清空打斷標記stop()否停止線程運行 不推薦suspend()否掛起線程 不推薦resume()否恢復線程運行 不推薦currentThread()是獲取當前線程

Object中與線程相關方法



方法名稱方法說明wait()/wait(long timeout)獲取到鎖的線程進入阻塞狀態notify()隨機喚醒被wait()的一個線程notifyAll();喚醒被wait()的所有線程,重新爭搶時間片

相關焦點

  • 萬字圖解Java多線程
    前言java多線程我個人覺得是javaSe中最難的一部分,我以前也是感覺學會了,但是真正有多線程的需求卻不知道怎麼下手,實際上還是對多線程這塊知識了解不深刻,不知道多線程api的應用場景,不知道多線程的運行流程等等,本篇文章將使用實例+圖解+源碼的方式來解析java多線程。
  • 圖解Java多線程
    圖解Java多線程筆記:http://tutorials.jenkov.com/java-concurrency/java-memory-model.htmlJava內存模型(JMM)定義了:how and when different threads can seevalues written to
  • 萬字圖解Java多線程(下)
    多個線程都是讀共享資源也是沒有問題的當多個線程讀寫共享資源時,如果發生指令交錯,就會出現問題臨界區: 一段代碼如果對共享資源的多線程讀寫操作,這段代碼就被稱為臨界區。注意的是 指令交錯指的是 java代碼在解析成字節碼文件時,java代碼的一行代碼在字節碼中可能有多行,在線程上下文切換時就有可能交錯。線程安全指的是多線程調用同一個對象的臨界區的方法時,對象的屬性值一定不會發生錯誤,這就是保證了線程安全。
  • 一起學JAVA——java多線程(其二)
    上一篇文章我們主要介紹了java多線程中的一些核心概念。今天我繼續介紹線程中的其他知識。線程同步互斥多個線程同時訪問或操作同一資源時,很容易出現數據前後不一致的問題。為了避免這樣的事情發生,我們要保證線程同步互斥,所謂同步互斥就是:並發執行的多個線程在某一時間內只允許一個線程在執行以訪問共享數據。Java中線程互斥的實現機制由多線程帶來的性能改善是以可靠性為代價的,所以編程出線程安全的類代碼是十分必要的。
  • JAVA多線程 集合同步
    copy-on-writecollections使用CopyOnWriteArrayList,它是完全的線程安全的集合類,包括其迭代器(Iterator & listIterator)也都是完全的線程安全,因此迭代(iteration)期間,不需要同步代碼塊,也不會拋出任何ConcurrentModificationException異常。
  • 由淺入深學習Java多線程和高並發
    實現多線程的兩種方法及源碼分析線程的生命周期及相關方法synchronized關鍵字volatile關鍵字Java內存模型一、實現多線程的兩種方法及源碼分析官方文檔說明,實現多線程的方法有且僅有兩種可重入:同一線程的外層函數獲得鎖之後,內層函數可以直接獲得鎖。不可中斷:一旦鎖被別的線程獲取了,本線程還想獲得鎖,只能永遠等待或者阻塞,直到別的線程釋放鎖。那不用synchronized的後果是什麼呢?比如兩個線程同時執行i++,各執行10萬次,按道理結果是20萬,但是實際結果是可能會小於20萬。不信?請看下面的代碼。
  • JAVA多線程-Semaphore
    因此可以協調線程,比如,如果線程1調用acquire()並且將一個對象插入到共享list中,線程2在從該列表中獲取對象之前調用了release(),實際上,你已經創建了一個阻塞隊列,信號量中可用的許可證數量將與阻塞隊列可以容納的最大元素數相對應公平
  • 太厲害了,阿里P8架構師良心出版最新Java多線程設計模式PDF
    前言提起多線程編程,恐怕許多開發人員都會搖頭表示不懂。確實,在校生和剛就職的開發人員往往很少有機會能夠實踐多線程編程。多數情況下,他們都是在開發框架下編寫單線程的業務代碼,而多線程的部分則被封裝在了框架內部。即使是經驗豐富的開發人員也會感嘆他們曾經在多線程上栽過的跟頭。但不可否認的是,多線程的確是- -把利器,活用多線程有助於提高程序的響應性和吞吐量。
  • Java多線程模式-immutable模式
    線程安全。1、實例在創建後,狀態不會發生變化比如上面的Person類,一旦創建,欄位不會發生變化,不發生變化需要結合具體業務涉及,對於人而言,sex/id不會發生變化,一出生就決定了,name發生變化得機率很小注意引用欄位
  • Java 多線程的創建方式
    java Vs OS線程 在Hotspot JVM中,Java線程和本機作業系統(即OS)線程之間存在直接映射,在為Java線程準備狀態(包括線程本地存儲,緩衝區分配,創建同步對象,堆棧和程序計數器)之後,將創建本機OS線程.
  • 阿里大牛,用300個案例肢解Java多線程和設計模式,你還不懂?
    (23種設計模式)多線程與並發處理是程序設計好壞優劣的重要課題,本書通過淺顯易懂的文字與實例來介紹與Java線程相關的設計模式理念,並且通過實際的Java程序範例和UML圖示來一一解說,書中在程序代碼的重要部分加上標註使讀者更加容易解讀。再配合眾多的說明圖解,無論對於初學者還是程序設計高手來說,這都是學習和認識設計模式的一本非常難得的參考書。
  • 好程式設計師Java培訓告訴你Java-線程怎麼來的?
    好程式設計師java培訓告訴你Java-線程怎麼來的?並發處理的廣泛應用是使得amdahl定律代替摩爾定律成為計算機性能發展源動力的根本原因,是人類壓榨計算機運算能力的最有力武器。  並發並非一定得用多線程,多進程也可以,不過java裡面談論並發,大多數與線程脫不開關係。因此我們從線程說起。
  • 好程式設計師Java培訓分享Java多線程常見面試問題
    在UNIX中你可以使用kill-3,然後threaddump將會列印日誌,在windows中你可以使用」CTRL+Break」。非常簡單和專業的線程面試問題,但是如果他問你怎樣分析它,就會很棘手。  3、你在多線程環境中遇到的共同的問題是什麼?你是怎麼解決它的?
  • 深度剖析——Java多線程
    注意:很多多線程是模擬出來的,真正的多線程是指多個cpu,即多核。如伺服器。;import java.io.IOException;import java.net.URL;/** * 練習Thread,實現多線程同步下載圖片 */public class TestThread2 extends Thread{ private String url;//網絡圖片地址 private String name;//保存文件名 public TestThread2
  • Java多線程
    1.2 多線程的優勢進程之間不能共享內存,但線程之間非常容易系統創建進程時需要為該進程重新分配系統資源,但創建線程則代價小得多,因此使用多線程效率更高Java語言內置了多線程功能2" + i); if(i == 20) { ft.run(); } } }}2.2 實現Runnable接口public class FirstThread implements java.lang.Runnable
  • 開發崗位這麼多,為什麼選Java?你學Java了嗎-開課吧
    軟體開發可以使用的語法是非常多,但是為什麼Java被廣泛的使用呢?其他程式語言與Java相比,Java語法相對簡單,並且是很多計算機語言的基礎。提到C++語言,很多人發現在使用過程中最容易出現的錯誤就是內存管理,而java有自動垃圾回收器,不用擔心內存。
  • Java多線程同步的五種方法
    閒話不多說,進入正題。二、為什麼要線程同步因為當我們有多個線程要同時訪問一個變量或對象時,如果這些線程中既有讀又有寫操作時,就會導致變量值或對象的狀態出現混亂,從而導致程序異常。舉個例子,如果一個銀行帳戶同時被兩個線程操作,一個取100塊,一個存錢100塊。假設帳戶原本有0塊,如果取錢線程和存錢線程同時發生,會出現什麼結果呢?
  • java多線程
    為什麼關注多線程呢?首先是面試經常被問到,然後是有很多並發工具類可以使用,如果理解不好,稀裡糊塗的使用更可怕。所以,多線程就像是個大山,層層迷霧,我不得要領,然而每次受挫,下一回又繼續。 我對於多線程的認識,來源有許多。一個是免費公開課,粗略的講多線程,而我要傻傻的記住wait()方法是釋放鎖在哪裡等待,sleep(arg)是持有鎖在睡覺,還有很多要記。
  • java開發兩年,連這些多線程知識都還沒掌握,你憑什麼漲薪
    System.out.println(i); } }TheadTest theadTest = new TheadTest(); theadTest.start();多線程的運行原理:多線程內存圖解:
  • 「軟帝學院」:15個頂級Java多線程面試題及答案
    多線程和並發的問題是任何java面試中必不可少的一部分。如果你想在股票投資銀行獲得任何前臺信息,你應該準備好很多的多線程問題。在投資銀行業務中,多線程和並發是一個非常熱門的話題,特別是在電子交易的開發中。他們會問面試官很多混淆java線程問題。面試官想知道面試官有足夠的java線程和並發的知識,因為很多考生只浮於表面。1)現在有三個線程:T1、T2和T3。