天天看點

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

作者 | 納達丶無忌

如果對什麼是線程、什麼是程序仍存有疑惑,請先 Google 之,因為這兩個概念不在本文的範圍之内。

用多線程隻有一個目的,那就是更好的利用 CPU 的資源,因為所有的多線程代碼都可以用單線程來實作。說這個話其實隻有一半對,因為反應“多角色”的程式代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模拟,當然也沒法說能用單線程來實作:比如最常見的“生産者,消費者模型”。

很多人都對其中的一些概念不夠明确,如同步、并發等等,讓我們先建立一個資料字典,以免産生誤會。

多線程:指的是這個程式(一個程序)運作時産生了不止一個線程

并行與并發:

并行:多個 CPU 執行個體或者多台機器同時執行一段處理邏輯,是真正的同時。

并發:通過 CPU 排程算法,讓使用者看上去同時執行,實際上從 CPU 操作層面不是真正的同時。并發往往在場景中有公用的資源,那麼針對這個公用的資源往往産生瓶頸,我們會用 TPS 或者 QPS 來反應這個系統的處理能力。

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

線程安全:經常用來描繪一段代碼。指在并發的情況之下,該代碼經過多線程使用,線程的排程順序不影響任何結果。這個時候使用多線程,我們隻需要關注系統的記憶體,CPU 是不是夠用即可。反過來,線程不安全就意味着線程的排程順序會影響最終結果,如不加事務的轉賬代碼:

void transferMoney(User from, User to, float amount){

to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);           

}

同步:Java 中的同步指的是通過人為的控制和排程,保證共享資源的多線程通路成為線程安全,來保證結果的準确。如上面的代碼簡單加入 @synchronized 關鍵字。在保證結果準确的同時,提高性能,才是優秀的程式。線程安全的優先級高于性能。

好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的内容:

  1. 紮好馬步:線程的狀态
  2. 内功心法:每個對象都有的方法(機制)
  3. 太祖長拳:基本線程類
  4. 九陰真經:進階多線程控制類

先來兩張圖:

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!
想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

各種狀态一目了然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀态的差別:

線程在 Running 的過程中可能會遇到阻塞 (Blocked) 情況

對 Running 狀态的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運作狀 (Runnable)。從 jdk 源碼注釋來看,blocked 指的是對 monitor 的等待(可以參考下文的圖)即該線程位于等待區。

線程在 Running 的過程中可能會遇到等待(Waiting)情況

線程可以主動調用 object.wait 或者 sleep,或者 join(join内部調用的是 sleep ,是以可看成 sleep 的一種)進入。從 jdk 源碼注釋來看,Waiting 是等待另一個線程完成某一個操作,如 join 等待另一個完成執行,object.wait() 等待object.notify() 方法執行。

Waiting 狀态和 Blocked 狀态有點費解,我個人的了解是:Blocked 其實也是一種 wait ,等待的是 monitor ,但是和Waiting 狀态不一樣,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀态,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。

從下文的 monitor 結構圖來差別:每個 Monitor 在某個時刻,隻能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分别在兩個隊列 “ Entry Set” 和 “Wait Set” 裡面等候。在 “Entry Set” 中等待的線程狀态 Blocked,從 jstack 的dump 中來看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的線程狀态是 Waiting,表現在 jstack 的 dump 中是 “in Object.wait()”。

此外,在 runnable 狀态的線程是處于被排程的線程,此時的排程順序是不一定的。Thread 類中的 yield 方法可以讓一個 running 狀态的線程轉入 runnable。

synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

他們是應用于同步問題的人工線程排程工具。講其本質,首先就要明确 monitor 的概念,Java 中的每個對象都有一個螢幕,來監測并發代碼的重入。在非多線程編碼時該螢幕不發揮作用,反之如果在 synchronized 範圍内,螢幕發揮作用。

wait/notify 必須存在于 synchronized 塊中。并且,這三個關鍵字針對的是同一個螢幕(某對象的螢幕)。這意味着 wait之後,其他線程可以進入同步塊執行。

當某代碼并不持有螢幕的使用權時(如圖中5的狀态,即脫離同步塊)去 wait 或 notify,會抛出java.lang.IllegalMonitorStateException。

也包括在 synchronized 塊中去調用另一個對象的 wait/notify,因為不同對象的螢幕不同,同樣會抛出此異常。

再講用法:

synchronized 單獨使用:

代碼塊:如下,在多線程環境下,synchronized 塊中的方法擷取了 lock 執行個體的 monitor,如果執行個體相同,那麼隻有一個線程能執行該塊内容

public class Thread1 implements Runnable { 
        Object lock; 
        public void run() { 
            synchronized(lock){ 
                ..do something 
            }
        } 
}
           

直接用于方法:相當于上面代碼中用 lock 來鎖定的效果,實際擷取的是 Thread1 類的 monitor。更進一步,如果修飾的是 static 方法,則鎖定該類所有執行個體

public class Thread1 implements Runnable {
            public synchronized void run() {
                ..do something
            }
}
           

synchronized, wait, notify 結合:典型場景生産者消費者問題

/**     
    * 生産者生産出來的産品交給店員     
    */    
    public synchronized void produce()    
    {
        if(this.product >= MAX_PRODUCT)
        { 
            try 
            {
                wait(); 
                System.out.println("産品已滿,請稍候再生産");
            }
            catch(InterruptedException e) 
            {
                e.printStackTrace () ; 
            } 
            return; 
        } 
        this.product++;
        System.out.println("生産者生産第" + this.product + "個産品.");
        notifyAll();   //通知等待區的消費者可以取出産品了
    }
  
    /**      
     * 消費者從店員取産品
    */    
    public synchronized void consume()     
    {        
        if(this.product <= MIN_PRODUCT)        
        {           
            try              
            {                
                wait(); 
                System.out.println("缺貨,稍候再取");
            }              
            catch (InterruptedException e)              
            {                 
                e.printStackTrace();             
            }            
            return;
        }                  
        
            System.out.println("消費者取走了第" + this.product + "個産品.");
            this.product--;
            notifyAll();   //通知等待去的生産者可以生産産品了
    }
           

volatile

多線程的記憶體模型:main memory(主存)、working memory(線程棧),在處理資料時,線程會把值從主存 load 到本地棧,完成操作後再 save 回去 (volatile 關鍵詞的作用:每次針對該變量的操作都激發一次 load and save) 。

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

針對多線程使用的變量如果不是 volatile 或者 final 修飾的,很有可能産生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一執行個體的同一屬性本身隻有一個副本。但是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。線上程安全的情況下加 volatile 會犧牲性能。

基本線程類指的是 Thread 類,Runnable 接口,Callable 接口

Thread 類實作了 Runnable 接口,啟動一個線程的方法:

MyThread my = new MyThread();
   my.start();
           

Thread類相關方法

//目前線程可轉讓 cpu 控制權,讓别的就緒狀态線程運作(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()   
//在一個線程中調用 other.join(),将等待other執行完後才繼續本線程。    
public join()
//後兩個函數皆可以被打斷
public interrupte()
           

關于中斷:它并不像 stop 方法那樣會中斷一個正在運作的線程。線程會不時地檢測中斷辨別位,以判斷線程是否應該被中斷(中斷辨別值是否為 true )。終端隻會影響到 wait 狀态、sleep 狀态和 join 狀态。被打斷的線程會抛出 InterruptedException。

Thread.interrupted() 檢查目前線程是否發生中斷,傳回boolean

synchronized 在獲鎖的過程中是不能被中斷的。

中斷是一個狀态!interrupt()方法隻是将這個狀态置為 true 而已。是以說正常運作的程式不去檢測狀态,就不會終止,而 wait 等阻塞方法會去檢查并抛出異常。如果在正常運作的程式中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體

Thread類最佳實踐:

寫的時候最好要設定線程名稱 Thread.name,并設定線程組 ThreadGroup,目的是友善管理。在出現問題的時候,列印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。

如何擷取線程中的異常

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

Runnable

與 Thread 類似

Callable

future 模式:并發模式的一種,可以有兩種形式,即無阻塞和阻塞,分别是 isDone 和 get。其中 Future 對象用來存放該線程的傳回值以及狀态

ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支援 callable 也能夠支援runnable 接口類型. 
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞 
future.get() // return 傳回值,阻塞直到該線程運作結束
           

以上都屬于内功心法,接下來是實際項目中常用到的工具了,Java1.5 提供了一個非常高效實用的多線程包: java.util.concurrent, 提供了大量進階工具,可以幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程式。

1.ThreadLocal類

用處:儲存線程的獨立變量。對一個線程類(繼承自 Thread )

當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,是以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用于使用者登入控制,如記錄 session 資訊。

實作:每個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 一樣,差別是桶裡放的是 entry 而不是 entry 的連結清單。功能還是一個 map 。)以本身為 key,以目标為 value。

主要方法是 get() 和 set(T a),set 之後在 map 裡維護一個threadLocal -> a,get 時将 a 傳回。ThreadLocal 是一個特殊的容器。

2.原子類(AtomicInteger、AtomicBoolean……)

如果使用 atomic wrapper class 如 atomicInteger,或者使用自己保證原子的操作,則等同于 synchronized

//傳回值為 boolean
AtomicInteger.compareAndSet(int expect,int update)
           

該方法可用于實作樂觀鎖,考慮文中最初提到的如下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,但是 b的加十元代碼約為:

if(b.value.compareAndSet(old, value)){  
    return ;
}else{
        //try again
        // if that fails, rollback and log
}
           

AtomicReference

對于 AtomicReference 來講,也許對象會出現,屬性丢失的情況,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。

這時候,AtomicStampedReference 就派上用場了。這也是一個很常用的思路,即加上版本号

3.Lock類

lock: 在 java.util.concurrent 包内。共有三個實作:

ReentrantLock

ReentrantReadWriteLock.ReadLock

ReentrantReadWriteLock.WriteLock

主要目的是和 synchronized 一樣, 兩者都是為了解決同步問題,處理資源争端而産生的技術。功能類似但有一些差別。

差別如下:

1.lock 更靈活,可以自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)

2.提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶逾時時間版本

3.本質上和螢幕鎖(即 synchronized 是一樣的)

4.能力越大,責任越大,必須控制好加鎖和解鎖,否則會導緻災難。

5.和 Condition 類的結合。

6.性能更高,對比如下圖:

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

可重入的意義在于持有鎖的線程可以繼續持有,并且要釋放對等的次數後才真正釋放該鎖。

使用方法是:

1.先 new 一個執行個體

static ReentrantLock r=new ReentrantLock();
           

2.加鎖

r.lock()或 r.lockInterruptibly();
           

此處也是個不同,後者可被打斷。當 a 線程 lock 後,b 線程阻塞,此時如果是 lockInterruptibly,那麼在調用 b.interrupt() 之後,b 線程退出阻塞,并放棄對資源的争搶,進入 catch 塊。(如果使用後者,必須 throw interruptable exception 或 catch)

3.釋放鎖

r.unlock()
           

必須做!何為必須做呢,要放在 finally 裡面。以防止異常跳出了正常流程,導緻災難。這裡補充一個小知識點,finally 是可以信任的:經過測試,哪怕是發生了 OutofMemoryError ,finally 塊中的語句執行也能夠得到保證。

ReentrantReadWriteLock

可重入讀寫鎖(讀寫鎖的一個實作)

ReentrantReadWriteLock   lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock(); 
WriteLock w = lock.writeLock();
           

兩者都有 lock,unlock 方法。寫寫,寫讀互斥;讀讀不互斥。可以實作并發讀的高效線程安全代碼

4.容器類

這裡就讨論比較常用的兩個:

BlockingQueue

ConcurrentHashMap

阻塞隊列。該類是 java.util.concurrent 包下的重要類,通過對 Queue 的學習可以得知,這個 queue 是單向隊列,可以在隊列頭添加元素和在隊尾删除或取出元素。類似于一個管道,特别适用于先進先出政策的一些應用場景。普通的 queue 接口主要實作有 PriorityQueue(優先隊列),有興趣可以研究

BlockingQueue 在隊列的基礎上添加了多線程協作的功能:

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

除了傳統的 queue 功能(表格左邊的兩列)之外,還提供了阻塞接口 put 和 take,帶逾時功能的阻塞接口 offer 和 poll。put 會在隊列滿的時候阻塞,直到有空間時被喚醒;take 在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用于生産者-消費者模型尤其好用,堪稱神器。

常見的阻塞隊列有:

ArrayListBlockingQueue

LinkedListBlockingQueue

DelayQueue

SynchronousQueue

ConcurrentHashMap

高效的線程安全哈希 map。請對比 hashTable , concurrentHashMap, HashMap

5.管理類

管理類的概念比較泛,用于管理線程,本身不是多線程的,但提供了一些機制來利用上述的工具做一些封裝。

了解到的值得一提的管理類:ThreadPoolExecutor 和 JMX架構下的系統級管理類 ThreadMXBean

ThreadPoolExecutor

如果不了解這個類,應該了解前面提到的 ExecutorService,開一個自己的線程池非常友善

ExecutorService e = Executors.newCachedThreadPool(); 
    ExecutorService e =Executors.newSingleThreadExecutor();  
    ExecutorService e = Executors.newFixedThreadPool(3);   
    // 第一種是可變大小線程池,按照任務數來配置設定線程,    
    // 第二種是單線程池,相當于 FixedThreadPool(1)    
    // 第三種是固定大小線程池。
    // 然後運作   
    e.execute(new MyRunnableImpl());
           

該類内部是通過 ThreadPoolExecutor 實作的,掌握該類有助于了解線程池的管理,本質上,他們都是 ThreadPoolExecutor 類的各種實作版本。請參見 javadoc:

想要金九銀十面試通關,不懂 Java多線程肯定是不行的!

翻譯一下:

corePoolSize: 池内線程初始值與最小值,就算是空閑狀态,也會保持該數量線程。

maximumPoolSize: 線程最大值,線程的增長始終不會超過該值。

keepAliveTime: 當池内線程數高于 corePoolSize 時,經過多少時間多餘的空閑線程才會被回收。回收前處于 wait 狀态

unit:

時間機關,可以使用 TimeUnit 的執行個體,如 TimeUnit.MILLISECONDS

workQueue: 待入任務(Runnable)的等待場所,該參數主要影響排程政策,如公平與否,是否産生餓死 (starving)

threadFactory: 線程工廠類,有預設實作,如果有自定義的需要則需要自己實作 ThreadFactory 接口并作為參數傳入。

歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支援!