Java中的多線程詳解

来源:http://www.cnblogs.com/bigbigheart/archive/2016/10/27/6005583.html
-Advertisement-
Play Games

如果對什麼是線程、什麼是進程仍存有疑惑,請先Google之,因為這兩個概念不在本文的範圍之內。 用多線程只有一個目的,那就是更好的利用cpu的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程式代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬 ...


如果對什麼是線程、什麼是進程仍存有疑惑,請先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關鍵字。在保證結果準確的同時,提高性能,才是優秀的程式。線程安全的優先順序高於性能。 
好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:

扎好馬步:線程的狀態 
內功心法:每個對象都有的方法(機制) 
太祖長拳:基本線程類 
九陰真經:高級多線程式控制制類 
扎好馬步:線程的狀態 
先來兩張圖: 
圖片描述
圖片描述
各種狀態一目瞭然,值得一提的是”blocked”這個狀態: 
線程在Running的過程中可能會遇到阻塞(Blocked)情況

調用join()和sleep()方法,sleep()時間結束或被打斷,join()中斷,IO完成都會回到Runnable狀態,等待JVM的調度。 
調用wait(),使該線程處於等待池(wait blocked pool),直到notify()/notifyAll(),線程被喚醒被放到鎖定池(lock blocked pool ),釋放同步鎖使線程回到可運行狀態(Runnable) 
對Running狀態的線程加同步鎖(Synchronized)使其進入(lock blocked pool ),同步鎖被釋放進入可運行狀態(Runnable)。 
此外,在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給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一樣, 兩者都是為瞭解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。

區別如下:

lock更靈活,可以自由定義多把鎖的枷鎖解鎖順序(synchronized要按照先加的後解順序) 
提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有trylock的帶超時時間版本。 
本質上和監視器鎖(即synchronized是一樣的) 
能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。 
和Condition類的結合。 
性能更高,對比如下圖:

圖片描述
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介面並作為參數傳入。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • RHEL7.2配置本地yum源 [root@localhost ~]#monut /dev/sr0 /mnt #掛載光碟 [root@localhost ~]# rm -rf /etc/yum.repos.d/* [root@localhost ~]# vim /etc/yum.repos.d/rh ...
  • 1、nginx入門篇 nginx安裝與基礎配置 nginx優化配置分析與說明 nginx模塊結構 2、nginx功能篇 配置nginx的gzip功能 配置nginx的rewrite功能 配置nginx的proxy功能 配置nginx的cache功能 配置nginx的郵件服務功能 3、nginx實現篇 ...
  • 在weblogic中部署項目通常有三種方式:第一,在控制臺中安裝部署;第二,將部署包放在domain域中autodeploy目錄下部署;第三,使用域中配置文件config.xml 進行項目的部署。 控制台部署 1 啟動weblogic服務,登錄到weblogic控制台頁面,輸入用戶名和密碼,登錄到控 ...
  • 主域控角色遷移和奪取(轉載) 轉載自:http://yupeizhi.blog.51cto.com/3157367/1427978 操作系統版本:Windows2012R2 數據中心版 FSMO角色遷移 主域控沒有掛的情況下使用FSMO角色遷移 FSMO角色遷移步驟,下麵步驟在備用域控上執行 0、使 ...
  • Permission deny 許可權 拒絕 查看許可權 ls -a ls -la expression 查看文件夾裡邊東西的許可權 用戶群的分類 組群:一個操作系統可能幾個人同時用 方便小組的文件安全和共用 用戶 組群(在一個組裡邊的) Others root rws rws rws 天神 使用者/ro ...
  • ...
  • 首先聲明,代碼是從一個大神的源碼里偷來的,我稍微整理了一下,現在可以通用。 作用是把你現有的資料庫表結構和數據生成yii2的遷移文件 1.下載模塊源碼解壓縮到 backend/modules/ 2.在 backend/config/main.php 添加如下配置 3.在你的後臺訪問 模塊下載地址 : ...
  • 創建線程的第一種方式: 這種方式的特點(缺陷):線程任務和線程是綁定在一起的。 示例: 四個視窗同時賣票 因為是同時,所以使用多線程。 創建四個線程,都是賣票。 因為都是賣票,所以四個線程的任務是一樣的。 只需要定義一個類繼承Thread。 為瞭解決四個線程共用票的問題,需要使用創建線程的第二種方式 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...