Java多線程核心知識

来源:https://www.cnblogs.com/chenshengjava/archive/2018/04/03/8710400.html
-Advertisement-
Play Games

多線程相對於其他 Java 知識點來講,有一定的學習門檻,並且瞭解起來比較費勁。在平時工作中如若使用不當會出現數據錯亂、執行效率低(還不如單線程去運行)或者死鎖程式掛掉等等問題,所以掌握瞭解多線程至關重要。 本文從基礎概念開始到最後的併發模型由淺入深,講解下線程方面的知識。 概念梳理 本節我將帶大家 ...


多線程相對於其他 Java 知識點來講,有一定的學習門檻,並且瞭解起來比較費勁。在平時工作中如若使用不當會出現數據錯亂、執行效率低(還不如單線程去運行)或者死鎖程式掛掉等等問題,所以掌握瞭解多線程至關重要。 

本文從基礎概念開始到最後的併發模型由淺入深,講解下線程方面的知識。 

概念梳理 

本節我將帶大家瞭解多線程中幾大基礎概念。 

併發與並行 

並行,表示兩個線程同時做事情。 
併發,表示一會做這個事情,一會做另一個事情,存在著調度。單核 CPU 不可能存在並行(微觀上)。 


臨界區 
臨界區用來表示一種公共資源或者說是共用數據,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區資源被占用,其他線程要想使用這個資源,就必須等待。 


阻塞與非阻塞 

阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個線程占用了臨界區資源,那麼其它所有需要這個資源的線程就必須在這個臨界區中進行等待,等待會導致線程掛起。這種情況就是阻塞。此時,如果占用資源的線程一直不願意釋放資源,那麼其它所有阻塞在這個臨界區上的線程都不能工作。阻塞是指線程在操作系統層面被掛起。阻塞一般性能不好,需大約8萬個時鐘周期來做調度。非阻塞則允許多個線程同時進入臨界區。 

死鎖 

死鎖是進程死鎖的簡稱,是指多個進程迴圈等待他方占有的資源而無限的僵持下去的局面。 


活鎖 

假設有兩個線程1、2,它們都需要資源 A/B,假設1號線程占有了 A 資源,2號線程占有了 B 資源;由於兩個線程都需要同時擁有這兩個資源才可以工作,為了避免死鎖,1號線程釋放了 A 資源占有鎖,2號線程釋放了 B 資源占有鎖;此時 AB 空閑,兩個線程又同時搶鎖,再次出現上述情況,此時發生了活鎖。簡單類比,電梯遇到人,一個進的一個出的,對面占路,兩個人同時往一個方向讓路,來回重覆,還是堵著路。如果線上應用遇到了活鎖問題,恭喜你中獎了,這類問題比較難排查。 

饑餓 

饑餓是指某一個或者多個線程因為種種原因無法獲得所需要的資源,導致一直無法執行。 

線程的生命周期 

線上程的生命周期中,它要經歷創建、可運行、不可運行幾種狀態。 

創建狀態 

當用 new 操作符創建一個新的線程對象時,該線程處於創建狀態。 
處於創建狀態的線程只是一個空的線程對象,系統不為它分配資源。 

可運行狀態 

執行線程的 start() 方法將為線程分配必須的系統資源,安排其運行,並調用線程體——run()方法,這樣就使得該線程處於可運行狀態(Runnable)。 
這一狀態並不是運行中狀態(Running),因為線程也許實際上並未真正運行。 

不可運行狀態 

當發生下列事件時,處於運行狀態的線程會轉入到不可運行狀態: 

  • 調用了 sleep() 方法;
  • 線程調用 wait() 方法等待特定條件的滿足;
  • 線程輸入/輸出阻塞;
  • 返回可運行狀態;
  • 處於睡眠狀態的線程在指定的時間過去後;
  • 如果線程在等待某一條件,另一個對象必須通過 notify() 或 notifyAll() 方法通知等待線程條件的改變;
  • 如果線程是因為輸入輸出阻塞,等待輸入輸出完成。

線程的優先順序 

線程優先順序及設置 

線程的優先順序是為了在多線程環境中便於系統對線程的調度,優先順序高的線程將優先執行。一個線程的優先順序設置遵從以下原則: 

  • 線程創建時,子繼承父的優先順序;
  • 線程創建後,可通過調用 setPriority() 方法改變優先順序;
  • 線程的優先順序是1-10之間的正整數。

線程的調度策略 

線程調度器選擇優先順序最高的線程運行。但是,如果發生以下情況,就會終止線程的運行: 

  • 線程體中調用了 yield() 方法,讓出了對 CPU 的占用權;
  • 線程體中調用了 sleep() 方法,使線程進入睡眠狀態;
  • 線程由於 I/O 操作而受阻塞;
  • 另一個更高優先順序的線程出現;
  • 在支持時間片的系統中,該線程的時間片用完。

單線程創建方式 

單線程創建方式比較簡單,一般只有兩種方式:繼承 Thread 類和實現 Runnable 介面;這兩種方式比較常用就不在 Demo 了,但是對於新手需要註意的問題有: 

  • 不管是繼承 Thread 類還是實現 Runable 介面,業務邏輯是寫在 run 方法裡面,線程啟動的時候是執行 start() 方法;
  • 開啟新的線程,不影響主線程的代碼執行順序也不會阻塞主線程的執行;
  • 新的線程和主線程的代碼執行順序是不能夠保證先後的;
  • 對於多線程程式,從微觀上來講某一時刻只有一個線程在工作,多線程目的是讓 CPU 忙起來;
  • 通過查看 Thread 的源碼可以看到,Thread 類是實現了 Runnable 介面的,所以這兩種本質上來講是一個;

PS:平時在工作中也可以借鑒這種代碼結構,對上層調用來講提供更多的選擇,作為服務提供方核心業務歸一維護 

為什麼要用線程池 

通過上面的介紹,完全可以開發一個多線程的程式,為什麼還要引入線程池呢。主要是因為上述單線程方式存在以下幾個問題: 

  • 線程的工作周期:線程創建所需時間為 T1,線程執行任務所需時間為 T2,線程銷毀所需時間為 T3,往往是 T1+T3 大於 T2,所有如果頻繁創建線程會損耗過多額外的時間;
  • 如果有任務來了,再去創建線程的話效率比較低,如果從一個池子中可以直接獲取可用的線程,那效率會有所提高。所以線程池省去了任務過來,要先創建線程再去執行的過程,節省了時間,提升了效率;
  • 線程池可以管理和控制線程,因為線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控;
  • 線程池提供隊列,存放緩衝等待執行的任務。

大致總結了上述的幾個原因,所以可以得出一個結論就是在平時工作中,如果要開發多線程程式,儘量要使用線程池的方式來創建和管理線程。 

通過線程池創建線程從調用 API 角度來說分為兩種,一種是原生的線程池,另外該一種是通過 Java 提供的併發包來創建,後者比較簡單,後者其實是對原生的線程池創建方式做了一次簡化包裝,讓調用者使用起來更方便,但道理都是一樣的。所以搞明白原生線程池的原理是非常重要的。 

ThreadPoolExecutor 

通過 ThreadPoolExecutor 創建線程池,API 如下所示: 

代碼 
  1. public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,                          long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);   


先來解釋下其中的參數含義(如果看的比較模糊可以大致有個印象,後面的圖是關鍵)。 

  • corePoolSize
  • 核心池的大小。

在創建了線程池後,預設情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,從這兩個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建 corePoolSize 個線程或者一個線程。預設情況下,在創建了線程池後,線程池中的線程數為0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到 corePoolSize 後,就會把到達的任務放到緩存隊列當中。 

  • maximumPoolSize

線程池最大線程數,這個參數也是一個非常重要的參數,它表示線上程池中最多能創建多少個線程。 

  • keepAliveTime

表示線程沒有任務執行時最多保持多久時間會終止。預設情況下,只有當線程池中的線程數大於 corePoolSize 時,keepAliveTime 才會起作用,直到線程池中的線程數不大於 corePoolSize,即當線程池中的線程數大於 corePoolSize 時,如果一個線程空閑的時間達到 keepAliveTime,則會終止,直到線程池中的線程數不超過 corePoolSize。 
但是如果調用了 allowCoreThreadTimeOut(boolean) 方法,線上程池中的線程數不大於 corePoolSize 時,keepAliveTime 參數也會起作用,直到線程池中的線程數為0。 

  • unit

參數 keepAliveTime 的時間單位。 

  • workQueue

一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裡的阻塞隊列有以下這幾種選擇:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。 

  • threadFactory

線程工廠,主要用來創建線程。 

  • handler

表示當拒絕處理任務時的策略,有以下四種取值: 

  • ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出 RejectedExecutionException 異常;
  • ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常;
  • ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重覆此過程);
  • ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務。

上面這些參數是如何配合工作的呢?請看下圖: 


註意圖上面的序號。 

簡單總結下線程池之間的參數協作分為以下幾步: 

  • 線程優先向 CorePool 中提交;
  • 在 Corepool 滿了之後,線程被提交到任務隊列,等待線程池空閑;
  • 在任務隊列滿了之後 corePool 還沒有空閑,那麼任務將被提交到 maxPool 中,如果 MaxPool 滿了之後執行 task 拒絕策略。

流程圖如下: 


以上就是原生線程池創建的核心原理。除了原生線程池之外併發包還提供了簡單的創建方式,上面也說了它們是對原生線程池的一種包裝,可以讓開發者簡單快捷的創建所需要的線程池。 
Executors 

newSingleThreadExecutor 
創建一個線程的線程池,在這個線程池中始終只有一個線程存在。如果線程池中的線程因為異常問題退出,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。 

newFixedThreadPool 
創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那麼線程池會補充一個新線程。 

newCachedThreadPool 
可根據實際情況,調整線程數量的線程池,線程池中的線程數量不確定,如果有空閑線程會優先選擇空閑線程,如果沒有空閑線程並且此時有任務提交會創建新的線程。在正常開發中並不推薦這個線程池,因為在極端情況下,會因為 newCachedThreadPool 創建過多線程而耗盡 CPU 和記憶體資源。 

newScheduledThreadPool 
此線程池可以指定固定數量的線程來周期性的去執行。比如通過 scheduleAtFixedRate 或者 scheduleWithFixedDelay 來指定周期時間。 
PS:另外在寫定時任務時(如果不用 Quartz 框架),最好採用這種線程池來做,因為它可以保證裡面始終是存在活的線程的。 

推薦使用 ThreadPoolExecutor 方式 

在阿裡的 Java 開發手冊時有一條是不推薦使用 Executors 去創建,而是推薦去使用 ThreadPoolExecutor 來創建線程池。 
這樣做的目的主要原因是:使用 Executors 創建線程池不會傳入核心參數,而是採用的預設值,這樣的話我們往往會忽略掉裡面參數的含義,如果業務場景要求比較苛刻的話,存在資源耗盡的風險;另外採用 ThreadPoolExecutor 的方式可以讓我們更加清楚地瞭解線程池的運行規則,不管是面試還是對技術成長都有莫大的好處。 
改了變數,其他線程可以立即知道。保證可見性的方法有以下幾種: 

  • volatile

加入 volatile 關鍵字的變數在進行彙編時會多出一個 lock 首碼指令,這個首碼指令相當於一個記憶體屏障,記憶體屏障可以保證記憶體操作的順序。當聲明為 volatile 的變數進行寫操作時,那麼這個變數需要將數據寫到主記憶體中。 
由於處理器會實現緩存一致性協議,所以寫到主記憶體後會導致其他處理器的緩存無效,也就是線程工作記憶體無效,需要從主記憶體中重新刷新數據。 

我有一個微信公眾號,經常會分享一些Java技術相關的乾貨;如果你喜歡我的分享,可以用微信搜索“Java團長”或者“javatuanzhang”關註。


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

-Advertisement-
Play Games
更多相關文章
  • 枚舉 蘋果官方文檔 "枚舉" 蘋果官方文檔中文翻譯 "枚舉" 枚舉語法 使用Switch語句來匹配枚舉值 如果不能為所有枚舉成員都提供一個 case,那你也可以提供一個==default==情況來包含那些不能被明確寫出的成員: 關聯值 如果對於一個枚舉成員的所有的相關值都被提取為常量,或如果都被提取 ...
  • Python Copy: 在Python語言中,分為淺拷貝和深拷貝兩種形式,也就是官方文檔中的Shadow copy和Deep copy。在對簡單的對象(object)進行複製時,兩者沒有區別,如下麵的代碼所示: 從簡單對象的淺copy和深copy可以看出,對原列表中的元素進行修改,copy的對象里 ...
  • Description L公司有N個工廠,由高到底分佈在一座山上。如圖所示,工廠1在山頂,工廠N在山腳。由於這座山處於高原內陸地區(乾燥少雨),L公司一般把產品直接堆放在露天,以節省費用。突然有一天,L公司的總裁L先生接到氣象部門的電話,被告知三天之後將有一場暴雨,於是L先生決定緊急在某些工廠建立一 ...
  • 一、Python語言的特性: 1.與C語言不通,Python語言是一門解釋性語言。程式在執行過程中,執行一步、編譯一步。 2.Python是一個動態類型語言,不需要定義變數的數據類型。 3.Python是一門強類型語言。(如果定義了一個變數,如果不人為的強制類型轉換,它永遠都是開始的那種數據類型) ...
  • 在Mybatis深入學習的一周中,總感覺跟著師傅的視屏講解什麼都能懂,但實際自己操作的時候才發現自己一臉懵逼,不知道從何入手。但還好自己做了點筆記。在此記錄一下自己淺度學習Mybatis遇到幾個小問題。 1.個人感覺學習Mybatis過程中最好使用log4j 日誌文件,這樣在你自己測試代碼的時候,能 ...
  • Android類載入機制 Dalvik虛擬機如同其他Java虛擬機一樣,在運行程式時首先需要將對應的類載入到記憶體中。而在Java標準的虛擬機中,類載入可以從class文件中讀取,也可以是其他形式的二進位流。因此,我們常常利用這一點,在程式運行時手動載入Class,從而達到代碼動態載入執行的目的。只不 ...
  • 作業一、多項式的加減運算 1、設計要點與自我分析 我設計的類圖 老師建議類圖 我設計了兩個類來進行多項式的計算,類Polynomial進行多項式的存儲和輸入輸出,第二個類進行多項式加減運算。而加減運算的類裡面只有方法,而且都是靜態方法,沒有存儲變數,感覺這個設計還是有些問題。之後我也參考了一下別人的 ...
  • Description P教授要去看奧運,但是他舍不下他的玩具,於是他決定把所有的玩具運到北京。他使用自己的壓縮器進行壓縮,其可以將任意物品變成一堆,再放到一種特殊的一維容器中。P教授有編號為1...N的N件玩具,第i件玩具經過壓縮後變成一維長度為Ci.為了方便整理,P教授要求在一個一維容器中的玩具 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...