天天看點

【轉載】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

他們是應用于同步問題的人工線程排程工具。講其本質,首先就要明确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) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。

如何擷取線程中的異常

【轉載】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給b2元,但是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. 性能更高,對比如下圖:

synchronized和Lock性能對比

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

阻塞隊列。該類是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:

ThreadPoolExecutor參數解釋

翻譯一下:

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

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

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

unit:

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

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

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

轉載自連結:https://www.jianshu.com/p/40d4c7aebd66

作者的原創文章,轉載須注明出處。原創文章歸作者所有,歡迎轉載,但是保留版權。對于轉載了部落客的原創文章,不标注出處的,作者将依法追究版權,請尊重作者的成果。