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

作者 | 納達丶無忌

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

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

很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。

多線程:指的是這個程序(一個進程)運行時產生了不止一個線程

並行與併發:

並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。

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

線程安全:經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,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. 九陰真經:高級多線程控制類

紮好馬步:線程的狀態

先來兩張圖:

各種狀態一目瞭然,值得一提的是 "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 是任何對象都具有的同步工具。讓我們先來了解他們

他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 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) 。

針對多線程使用的變量如果不是 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) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。

如何獲取線程中的異常

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.性能更高,對比如下圖:

ReentrantLock

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

使用方法是:

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

BlockingQueue

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

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

除了傳統的 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:

翻譯一下:

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

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

keepAliveTime: 當池內線程數高於 corePoolSize 時,經過多少時間多餘的空閒線程纔會被回收。回收前處於 wait 狀態

unit

時間單位,可以使用 TimeUnit 的實例,如 TimeUnit.MILLISECONDS 

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

threadFactory: 線程工廠類,有默認實現,如果有自定義的需要則需要自己實現 ThreadFactory 接口並作爲參數傳入。

歡迎大家一起交流,喜歡文章記得關注我點個喜歡喲,感謝支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章