併發編程那點兒事

来源:https://www.cnblogs.com/chuan2/archive/2023/03/09/17201122.html
-Advertisement-
Play Games

線程理論 線程和進程的區別 進程 進程是操作系統分配資源的最小單位,每個進程都是一個在運行中的程式,在windows中一個運行的xx.exe就是一個進程,他們都擁有自己獨立的一塊記憶體空間,一個進程可以有多個線程 線程 線程是操作系統調度的最小單元,負責當前進程中程式的執行,一個進程可以運行多個線程, ...


目錄

線程理論

線程和進程的區別

進程

進程是操作系統分配資源的最小單位,每個進程都是一個在運行中的程式,在windows中一個運行的xx.exe就是一個進程,他們都擁有自己獨立的一塊記憶體空間,一個進程可以有多個線程

線程

線程是操作系統調度的最小單元,負責當前進程中程式的執行,一個進程可以運行多個線程,多個線程之間可以共用數據

  1. 本質區別:進程是操作系統資源分配的最小單元,線程是處理器任務調度和執行的最小單位
  2. 資源開銷:每個進程都有獨立的代碼和數據空間,進程之間的切換會有較大的開銷,線程可以看做輕量級的進程

進程間通訊

管道

管道是內核管理的一個緩衝區,相當於記憶體中一個的小紙條,管道的一端連接著一個進程的輸入,一端連接著另一個進程的輸出,當管道中沒有信息的話,從管道中讀的進程會阻塞,直到另一端的進程放入信息,當管道中信息放滿時,嘗試放入信息的進程會阻塞,直到另一個端進程取出信息,當兩個進程都結束時,管道也就結束了

特點:單向的,一端輸入一端輸出,採用先進先出FIFO模式,大小4K,滿時寫阻塞,空時讀阻塞

分類:普通管道(僅父子進程間通訊)位於記憶體,命名管道位於文件系統,沒有情緣關係的管道只要知道管道名也可以通訊

消息隊列

消息隊列可以看做是一個消息鏈表,只要線程有足夠的許可權,就可以往消息隊列裡面存消息和取消息,他獨立於發送進程和接收進程,提供了一種從一個進程向另一個進程發送數據塊的方法,每一個數據塊都有一個消息類型(頻道)和消息內容(節目),每個類型相互不受影響,類似於一個獨立的管道

特點:全雙工可讀可寫,生命周期跟隨內核,每個數據塊都有一個類型,接收者可以有不同的類型值,每個消息的最大長度是有上限的(MSGMAX),位元組數也是有上限的(MSGMNB),系統上的消息隊列總數也是有上限的(MSGMNI),

信號量

信號量本質上是一個計數器,它不以傳送數據為目的,主要是用來保護共用資源,使得資源在一個時刻只有一個進程獨享

原理:信號量只有等待和發送兩種操作,即P(sv)和V(sv)兩個操作都屬於原子操作

P(sv):如果sv的值大於0,就給它減1;如果它的值為0,就掛起該進程的執行

S(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因sv而掛起,就給他加1

共用記憶體

共用記憶體就是允許兩個進程或多個進程共用一定的存儲區,當一個進程改變了這個記憶體區域中的內容時,其他進程都會覺察到這個更改

特點:數據不需要在客戶端和服務端之間來回覆制,數據直接寫到記憶體,減少了數次數據拷貝,是很快的一種IPC

缺點:共用記憶體沒有任何的同步和互斥機制,需要使用信號量來實現對共用記憶體的存取和同步

套接字

套接字是一種允許兩個不同進程進行通信的編程介面,通過套接字介面,可以使一臺機器之間的進程可以相互通信,也可以使不同機器上的進程進行網路通信,套接字明確把客戶端和服務端分開,實現了多個客戶端連接到一個服務端

原理:伺服器端應用程式調用socket創建一個套接字,它是系統分配給伺服器進程的類似文件描述符的資源,不能與其他進程共用,伺服器進程給這個套接字起一個名字,本地套接字的名字是Linux文件系統中的文件名,一般在/tmp或/usr/tmp中,網路套接字的名字與客戶端連接的特定網路有關的服務標識符(埠號),系統調用bind給套接字命名後,伺服器進程就開始等待客戶端連接到命名套接字,系統調用一個listen創建一個隊列,用於存放來自客戶端的進入連接,伺服器用accept來接收客戶端的連接,當有客戶端連接時,伺服器進程會創建一個與原有命名不同的新的套接字,這個套接字只用於與這個特定的客戶端進行通信,原有套接字繼續處理來自其他客戶端的連接。

套接字的域:

  1. AF_INET域:internet網路,是Novell NetWare網路協議的實現,也就是我們常說的IPV4
  2. AF_INET6域:internet網路,是Novell NetWare網路協議的實現,我們常說的IPV6
  3. AF_UNIX:unix文件系統域,底層協議的文件的輸入和輸出,地址就是文件名
  4. AF_ISO域:基於ISO標準協議的網路
  5. AF_XFS域:基於施樂(Xerox)的網路

​ AF_INET域中的類型

  1. 流套接字:通過TCP/IP連接實現,提供一個有序,可靠,雙向位元組流的連接,保證了發送的數據不會丟失,複製,或亂序到達
  2. 數據報套接字:通過UDP/IP實現,提供一個無序的,不可靠的服務,他不需要建立和維護連接,作為一個單獨的網路消息被傳輸,可能會丟失,複製或亂序的到達,但開銷很小,適用於單次查詢,不保留連接信息

消息隊列和管道的區別

  1. 管道是跟隨進程的,消息隊列是跟隨內核的,也就是說進程結束後,管道就結束了,但消息隊列還會存在
  2. 管道是文件,訪問速度慢,消息隊列是數據結構存放在記憶體,訪問速度快
  3. 管道是數據流式存取,消息隊列是數據塊式存取

線程間通信

共用記憶體

線程之間共用程式的公共狀態,通過讀寫這個公共狀態來進行隱式通信,這會經歷兩個過程

  1. 線程A把本地記憶體A更新過的共用變數刷新到主記憶體中
  2. 線程B到主記憶體中讀取線程A更新後的共用變數

消息傳遞

線程之間通過發送消息來顯示的進行通信,比如wait()和notify()方法

線程的五種狀態和生命周期

線程被創建並啟動後,他既不是一啟動就進入執行狀態,也不是一直處於執行狀態,線上程的生命周期中,他要經過新建(new),就緒(Runnable),運行(Running),阻塞(Blocked)和死亡(Dead)5種狀態,在啟動過後,他不可能會一直占用,所以狀態會在運行和阻塞之間切換

  1. 新建狀態(NEW):當程式使用new關鍵字創建了一個線程之後,該線程就處於新建狀態,此時僅有JVM分配記憶體,並初始化其成員變數的值
  2. 就緒狀態(EUNNABLE):當線程對象調用了start()方法之後,該線程處於就緒狀態,JAVA虛擬機會為其創建方法調用棧和程式計數器,等待調度運行
  3. 運行狀態(RUNNING):如果處於就緒狀態的線程獲得了CPU,開始執行run方法的線程執行體,則該線程處於運行狀態
  4. 阻塞狀態(BLOCKED):阻塞狀態是指線程因為某種原因放棄了cpu使用權,也就是讓出了cpu timeslice,暫時停止運行,直到線程進入可可運行(Runtime)狀態,才有機會獲得cpu timeslice轉到運行running狀態
  5. 死亡狀態(sleep/join):線程運行結束最後的狀態

線程阻塞的三種情況

  1. 等待阻塞:運行的線程執行wait方法,JVM會把該線程放入等待隊列中
  2. 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中
  3. 其他阻塞:運行的線程執行Thread.sleep()或者t.join()方法或者發出了I/O請求時,JVM會把該線程變成阻塞狀態,當sleep或者join結束,I/O處理完成時,線程就會重新轉入可運行狀態

線程結束的三種方式

  1. run()或者call()方法執行完成,線程正常結束
  2. 線程拋出一個未捕獲的Exception或Error
  3. 線程調用stop()方法來結束該線程-該方法通常容易導致死鎖,不推薦使用

線程的上下文

線程的上下文是指某一個時間點寄存器和程式計數器的內容,上下文切換可以認為是內核(操作系統的核心)在CPU上對於進程(包括線程)進行切換,上下文切換過程中的信息是保存在進程式控制制塊(PCB,process control block)的,PCB也被稱作為切換楨

上下文的切換過程

  1. 掛起一個進程,將這個進程在CPU中的狀態(上下文)存儲在記憶體中
  2. 在記憶體中檢索下一個進程的上下文,並將其在CPU的寄存器中恢復
  3. 跳轉到程式計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程在程式中

上下文切換的原因

  1. 當前執行任務的時間片用完之後,系統CPU正常調度下一個任務
  2. 當前執行任務碰到IO阻塞,調度器將此任務掛起,繼續下一個任務
  3. 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一個任務
  4. 用戶代碼掛起當前任務,讓出CPU時間
  5. 硬體中斷

寄存器是CPU內部數量較少但是速度很快的記憶體,

程式計數器是一個專用的寄存器,用於表明指令序列中CPU正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統

線程調度器

線程調度器是一個操作系統服務,它負責為Runnable狀態的線程分配CPU時間片,當一個線程被創建和啟動後,它的執行便依賴於線程調度器,分配cpu時間可與基於線程的優先順序或者線程等待的時間

線程調度類型

搶占式調度

搶占式調度指的是每條線程執行的時間,線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是

協同式調度

協同式調度指某一個線程執行完成之後主動通知系統切換到一個線程上執行,這種模式就像接力賽一樣,一個接一個,線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步的問題,缺點是如果一個線程編寫有問題,一直在運行中,那麼可能導致整個系統崩潰

調度演算法

先進先出演算法(FIFO)

按照任務進入隊列的順序,依次調用,執行完一個任務再執行下一個任務,只有當任務結束後才切換到下一個任務

優點:最小的任務切換開銷(沒有在任務執行中發生切換),最大的吞吐量(因為沒任務切換開銷),最朴實的公平性(先來先做)

缺點:平均響應時間高

適用場景:隊列中任務耗時差不多的場景

最短耗時任務優先演算法(SJF)

按照任務的耗時長短進行調度,優先調度耗時最短的任務,這個演算法的前提是知道每個任務的耗時時間,需要註意的是耗時最短指的是剩餘執行時間,解決了先進先出演算法中短耗時任務等待長耗時任務的窘境

優點:平均響應時間低

缺點:耗時長的任務遲遲得不到調度,不公平,容易形成饑餓,頻繁的任務切換,調度的額外開銷大

時間片輪轉演算法(Round Robin)

給隊列中的每個任務分配一個時間片,當時間片到了之後將此任務放到隊列的尾部,切換到下一個任務執行,解決了SJF中長耗時任務饑餓的問題

優點:每個任務都能夠得到公平的調度,耗時短的任務即使落在耗時長的任務後面,也能夠較快的得到調度執行

缺點:任務切換開銷大,需要多次切換任務上下文,時間片不好設置

適用場景:隊列中耗時差不多的任務

JVM的線程調度實現

java使用的線程調度是搶占式調度,java中線程會按優先順序分配cpu時間片,優先順序越高越先執行,但是優先順序高的線程並不能獨自占用cpu時間片,只能是得到更多的cpu時間片,反之,優先順序低的線程分到的執行時間少,但不會分配不到執行時間

線程調度器讓線程讓出cpu的情況

  1. 當前運行線程主動讓出CPU,JVM暫時放棄CPU操作,例如調用yiled()方法
  2. 當前運行線程因為某種原因進入阻塞狀態,例如阻塞在I/O上
  3. 當前運行線程結束,也就是運行完run方法裡面的任務

守護線程和用戶線程的區別

任何線程都可以設置為守護線程(Daemon)和用戶線程(User),預設情況下新建的線程是用戶線程,通過setDaemon(true)可以將線程設置為守護線程,這個函數必須線上程啟動前進行調用,否則會報錯,守護線程依賴於用戶線程,當用戶線程都退出了,守護線程也就退出了,典型的守護線程就是垃圾回收線程

線程安全

線程安全是某個函數,函數庫在多線程的環境中被調用,能夠正確的處理多個線程之間的共用變數,不會對共用資源產生衝突,不會影響程式的運行結果

線程不安全的原因

  • 線程切換帶來的原子性問題
  • 緩存導致的可見性問題
  • 編譯優化帶來的有序性問題

解決方法

  • 原子性問題:JDK Atomic開頭的原子類,synchronized,LOCK
  • 可見性問題: synchronized,volatile,LOCK
  • 有序性問題:Happens-Before規則

線程實踐

創建線程的四種方式

  1. 繼承Thread類
  2. 實現Runnable介面
  3. 通過Callable和Future創建線程
  4. 通過線程池創建

幾種方式的區別

  • 實現Runnable,Callable介面的方式創建多線程

優勢:線程類只是實現了Runnable介面或者Callable介面,還可以繼承其他類,在這種方式下,多個線程可以共用同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU,代碼和數據分開,形成清晰的模型,較好的體現了面向對象的思想

劣勢:編程稍微複雜,如果要訪問當前線程,則必須Thread.currentThread()方法

  • 繼承Thread類的方式創建多線程

優勢:編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程

劣勢:繼承了Thread類,不能再繼承其他類

  • Runnable和Callable的區別
  1. Callable重寫的方法是call()方法,Runnable重寫的方法是run()方法
  2. Callable的任務執行後可以返回值,Runnable的任務不能返回值
  3. Call方法可以拋出異常,run方法不能拋出異常
  4. 運行Callable任務可以拿到一個Future對象,表示非同步任務的結果,提供了檢查任務是否完成的方法,獲取任務的結果,通過Future可以瞭解任務的執行情況,可以取消正在執行的任務

線程的基本方法

wait

使線程進入waiting狀態,只有等待另外線程的通知或者被中斷才會返回,調用會釋放對象的鎖,因此wait方法一般用在同步方法或者同步代碼塊中

sleep

使當前線程休眠,與wait方法不同的是sleep不會釋放當前鎖,會使線程進入timed-wating狀態

yield

使當前線程從執行狀態變為就緒狀態,也就是當前線程讓出本次cpu時間片,與其他線程一起重新競爭CPU時間片

interrupt

中斷一個線程,本質是給這個線程發行一個終止通知信號,影響這個線程內部的一個中斷標識位,這個線程本身並不會因此而改變狀態,註意線上程處於timed-wating狀態時調用interrupt會拋出InterruptedException,使線程提前結束timed-wating狀態

中斷狀態是線程固有的一個標識位,通過isInterrupted來獲取標識位的值,根據值然後調用thread.interruptd方法來安全的終止線程

join

等待其他線程終止,在當前線程中調用一個線程的join()方法,則當前線程轉為阻塞狀態,回到另一個線程結束,當前線程再由阻塞狀態變為就緒狀態,等待cpu分配,適用於主線程啟動了子線程,需要用到子線程返回的結果,也就是主線程需要在子線程結束後再結束,這個時候就可以使用join方法

notify

線程喚醒,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中的一個線程,選擇是任意的

其他方法

  1. isAlive():判斷一個線程是否存在

  2. activeCount():獲取程式中活躍的線程數

  3. enumerate():枚舉程式中的線程

  4. currentThread():得到當前線程

  5. isDaemon():判斷當前線程是否為一個守護線程

  6. setDaemon():設置一個線程為守護線程

  7. setName():為一個線程設置一個名稱

  8. setPriority():設置一個線程的優先順序

  9. getPriority():獲得一個線程的優先順序

sleep()和wait()的區別

類不同:sleep方法屬於Thread類中,wait方法屬於Object類

釋放鎖:sleep不會釋放鎖,wait會釋放鎖,sleep不釋放鎖到指定時間就會自動喚醒,wait不會自動喚醒

應用場景不同:wait被用於通信,sleep被用於暫停執行

run()和start()的區別

start方法被用來啟動新創建的線程,內部調用了run方法,這和直接調用run方法效果不一樣,直接調用run方法的時候,只會是在原來的線程中調用,沒有新的線程啟動,start()方法會創建新的線程

notifyAll和notify的區別

notify只會喚醒一個線程,notiyfAll會喚醒所有線程

notify可能會導致死鎖,而notifyAll則不會,notify是對notifyAll的一個優化

interrupted和isinterrupted的區別

interrupted會將中斷狀態清除,而isinterrupted不會,java多線程中的中斷機制使用這個內部標識符來實現,當一個中斷線程調用Thread.interrupt()來獲取中斷狀態時,中斷狀態會被清除,並設置為true,而非靜態方法調用isinterrupted用來查詢其他線程的中斷狀態不會改變中斷狀態的標識,任何拋出InterruptedException異常的方法都會將中斷狀態清零

實現線程同步的方法

線程同步指線程之間的一種制約關係,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒,線程的同步方法大體可以分為兩類,用戶模式和內核模式,內核模式指的是利用系統內核對象的單一性來進行同步,使用時需要切換內核態和用戶態,例如事件,信號量,互斥量,用戶模式不需要切換到內核態,例如原子操作(單一的一個全局變數),臨界區

併發理論

三要素

  • 原子性: 一個或多個操作要麼全部執行,要麼全部不執行
  • 可見性: 一個線程對共用變數的修改,另一個線程可以能夠立刻看見
  • 有序性: 程式執行的順序按照代碼的先後順序執行

並行和併發

  • 併發:兩個或多個(事件,任務,程式)在同一時刻發生,在一個處理器上交替執行這個任務,只是交替時間非常短,在巨集觀上是達到同時的現象
  • 並行:在同一時刻,有多條指令在多個處理器上同時執行,在巨集觀和微觀上都是一起執行的

併發關鍵字

synchronized

synchronized可以把任意一個非NULL的對象當做鎖,屬於獨占式的悲觀鎖,同時也是可重入鎖

作用

synchronized關鍵字是用來控制線程同步的,在多線程的環境下,控制synchronized代碼段不被多個線程同時執行

作用範圍

  1. 方法:鎖住的是對象的實例(this)
  2. 靜態方法:鎖住的是Class實例,又因為Class的相關數據存儲在雲數據空間,是全局共用的,所以靜態方法相當類的一個全局鎖,會鎖住所有調用該類的方法
  3. 某一個對象實例:鎖住的是所有以該對象為鎖的代碼塊,他有多個隊列,當有多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中

核心組件

  1. Wait Set:那些調用wait方法被阻塞的線程被放置這裡面
  2. Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中
  3. Entry List:存儲在Contention List中有資格成為候選資源的線程
  4. OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程就存在OnDeck中
  5. Owner:當前已經獲取到鎖資源的線程
  6. !Owner:當前釋放鎖的線程

實現

  1. JVM每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但在併發的情況下,ContentionList會被大量的併發線程進行CAS訪問,為了降低對尾部元素的競爭,JVM會將一部分線程移動到EntryList中作為候選競爭線程
  2. Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程為OnDeck線程(一般是最先進去的那個線程)
  3. Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權力交給OnDeck,OnDeck需要重新競爭鎖,這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM中,也把這種行為稱之為“競爭切換”
  4. OnDeck線程獲取到鎖資源會變為Owner線程,而沒有獲取到鎖資源的仍然停留在EntryList中,如果OWner線程被wait方法阻塞,則轉移到WaitSet隊列中,直到某個時刻通過notify或者notifyAll喚醒,重新進入到EntryList中
  5. 處於ContentionList,EntryList,WaitSet中的線程都處於阻塞狀態,該阻塞是有操作系統來完成的(Linux 內核下採用pthread_mutex_lock內核函數實現的)
  6. Synchronized是非公平鎖,Synchrionized線上程進入ContentionList時,等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進入ContentionList中,這對於已經進入隊列的線程是不公平的,還有一個不公平的就是在自旋獲取鎖的線程還可能會直接搶占OnDeck線程的資源
  7. 每個對象都有一個monitor對象,加鎖就在競爭monitor對象,代碼塊加鎖就在前後分別上monitorenter和monitorexit指令來實現,方法加鎖是通過一個標識位來實現
  8. synchronized是一個重量級鎖,需要調用操作系統相關的介面,性能是低效的,給線程加鎖消耗的時間比有用操作消耗的時間更多
  9. 在java1.6 ,synchronized進行了很多的優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖及偏向鎖等,效率有了本質上的提高,之後在java1.7和java1.8中均對該關鍵字的實現機製做了優化,引入了偏向鎖和輕量級鎖,都是在對象頭中有標記位,不需要經過操作系統加鎖
  10. 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,這種升級過程叫做鎖膨脹
  11. JDK1.6中預設是開啟偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖

volatile

volatile相比synchronized更加輕量

volatile修飾的變數具有synchronized的可見性,但是不具備原子性,也就是線程能夠自動發現volatile變數的最新值

volatile禁止了指令重排,在執行程式時,為了提升性能,處理器和編譯器常常會對指令進行重排,指令重排雖然不會影響單線程的執行結果,但是會破壞多線程的執行語義,指令重排有兩個條件,1在單線程環境下不能改變程式的運行結果,2存在數據依賴關係的情況不允許指令重排

適用場景:一個變數被多個線程共用,線程直接給這個變數賦值,例如狀態標記量和單例模式的雙檢鎖

ThreadLocal

ThreadLocal是一個本地線程副本變數工具類,主要用於將私有線程和該線程存放的副本對象做一個映射,各個線程之間的變數互不幹擾,在高併發的場景下,可以實現無狀態的調用,特別適用於各個線程依賴不同變數值完成操作的場景,是一種空間換時間的做法,在每個Thread裡面維護了一個以開地址實現的ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共用,自然也就沒有線程安全的問題了

基本方法

  1. get()獲取ThreadLocal在當前線程中保存的變數副本
  2. set():設置當前線程中變數的副本值
  3. remove():移除當前線程中變數的副本
  4. initialValue:延遲載入方法

應用場景:

  1. 資料庫連接,Session管理

final

特點

  1. 被final修飾的類不能被繼承
  2. 被final修飾的方法不可以被重寫
  3. 被final修飾的變數不可以被改變,如果修飾引用,那麼表示引用不可變,引用指向的內容可變
  4. 被final修飾的方法,JVM會嘗試將其內聯,以提高運行效率
  5. 被final修飾的常量,在編譯階段會存入常量池

Synchronized和volatile的區別

volatile應用在多個線程對實例變數更改的場合,刷新主記憶體共用變數的值從而使得各個線程可以獲得最新的值,線程讀取變數的值需要從主記憶體中讀取;synchronized是鎖定當前變數,只有當前線程可以該變數,其他線程被阻塞住,synchronize會創建一個記憶體屏障,記憶體屏障保證了所有CPU操作結果都會直接刷到主記憶體中,從而保證了操作的記憶體可見性

volatile僅能使用在變數級別,synchronized則可以使用在變數,方法,和類級別

volatile不會造成線程的阻塞,synchronized會造成線程的阻塞

volatile只能保證變數的可見性不能保證原子性,synchronized保證了變數的可見性和原子性

volatile標記的變數不會編譯器優化,可以禁止指令重排,synchronized標記的變數可以被編譯器優化

鎖理論

悲觀鎖

悲觀鎖是一種悲觀思想,總是假設最壞的情況,即認為每次都是線程不安全的,每次讀寫都會進行加鎖,synchronized就是悲觀鎖的實現

樂觀鎖

樂觀鎖是一種樂觀思想,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,只是在更新的時候判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制實現,適合於多讀的應用場景,在資料庫中write_condition機制就是樂觀鎖的實現,java中java.util.concurrent.atomic包下麵的原子變數類也是使用了樂觀鎖的一種CAS實現方法

死鎖

死鎖的四個條件

  1. 互斥條件:一個資源每次只能被一個線程使用
  2. 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放
  3. 不剝奪條件:進程已經獲得,在未使用完之前,不能強行剝奪
  4. 迴圈等待條件:若幹線程之間形成一種頭尾相接的迴圈等待資源關係

如何避免死鎖

  • 指定獲得鎖的順序,按照同一順序獲得訪問資源,類似於串列執行

公平鎖與非公平鎖

公平鎖加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得

非公平鎖加鎖時不考慮等待問題,直接嘗試獲取鎖,獲取不到則自動到隊尾等待

在相同條件下,非公平鎖的性能比公平鎖高5-10倍,因為公平鎖在多核的情況下需要維護一個隊列,增加了開銷

synchronized是非公平鎖,ReentrantLock預設的lock()方法採用的也是非公平鎖

共用鎖和獨享鎖

java併發包提供的加鎖模式分為獨占鎖和共用鎖

獨占鎖模式下,每次只能有一個線程能持有鎖,ReentrantLock就是以獨占方式實現的互斥鎖,獨占鎖採用悲觀鎖的加鎖策略,避免了讀/讀寫衝突,如果某個只讀線程獲取了鎖,則其他線程只能等待,這種情況其實不需要加鎖,因為讀操作並不會影響數據的一致性

共用鎖允許多個線程同時獲取鎖,併發訪問共用資源,加鎖策略是樂觀鎖

可重入鎖(遞歸鎖)

可重入鎖指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響,ReentrantLock和Synchronized都是可重入鎖

自旋鎖

自旋鎖是指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈,獲取鎖的線程一直處於活躍狀態

缺點:

  1. 如果某個線程持有鎖的時間過長,就會導致其他等待獲取鎖的線程進入迴圈等待,消耗CPU,使用不當會造成CPU使用率極高
  2. 自旋鎖是不公平的,無法滿足等待時間最長的線程優先獲取鎖,不公平的鎖就會存線上程餓死的問題

優點:

  1. 不會是線程狀態發生切換,一直處於用戶態,線程一直處於avtive的,不會使線程進入阻塞狀態,減少了不必要的上下文切換

輕量級鎖

鎖的狀態有四種:無鎖狀態,偏向鎖,輕量級鎖,重量級鎖

輕量級是相對於使用操作系統互斥量來實現的傳統鎖而言,輕量鎖不是用來代替重量級鎖的,他的本意是在沒有多線程競爭的前提下,減少傳統重量級鎖使用產生的性能消耗,適用於線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖升級為重量級鎖

鎖升級

隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,鎖的升級是單向的,只能從低到高,不會出現鎖降級

重量級鎖

Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現,但是監視器鎖本質是依賴於底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換這就需要從用戶態轉換到內核態,這個成本非常高,狀態之間的轉換需要相對比較長的時間 ,這也就Synchronized效率慢的原因,這依賴於操作系統Mutex Lock所實現的鎖稱之為重量級鎖,JDK1,6之後為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了輕量級鎖和偏向鎖

偏向鎖

偏向鎖的引入是為了在某個線程獲得鎖之後,消除這個線程的鎖重入(CAS)的開銷,看起來讓這個線程得到了偏向,減少不必要的輕量級鎖的執行路徑,因為輕量級鎖的獲取和釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,輕量級鎖是為了線上程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高性能

讀寫鎖

讀寫鎖分為讀鎖和寫鎖,讀鎖是無阻塞的,在沒有寫的情況下使用可以提高程式的效率,讀鎖和讀鎖不互斥,讀鎖和寫鎖互斥,寫鎖和寫鎖互斥

讀鎖適用於多個線程同時讀,但不能同時寫

寫鎖適用於只能一個線程寫數據,且其他線程不能讀取

分段鎖

分段鎖不是一種實際的鎖,而是一種思想,ConcurrenHashMap就是使用的分段鎖

Semaphore

Semaphore是一種基於計數的信號量,可以設定一個閾值,根據這個閾值,多個線程競爭獲取許可信號,做完自己的申請後歸還,超過閾值後,申請許可信號量的線程將會阻塞,Semaphore可以用來構建一些對象池,資源池之類的,也可以創建一個計數為1的Semaphore二元信號量,類似互斥鎖的機制

CAS

CAS(Compare And Swap/Set)比較並交換,CAS是一種基於鎖的操作,CAS操作包含三個參數(V,A,B),記憶體位置(V),預期原值(A)和新值(B),如果記憶體地址裡面的值和A的值一樣,那麼就將記憶體裡面的值更新成B

CAS操作採用的是樂觀鎖,,通過無限迴圈來獲取鎖,如果在第一輪迴圈中,A線程的值被B線程修改了,那麼A線程需要自旋,到下次迴圈才有機會執行

缺點:

  1. 會導致ABA問題:線程1從記憶體位置V取出A,這個時候線程2也從記憶體位置V取出A,將A變成B,然後又變成A,這個時候線程1再進行CAS,發現還是A,然後線程1CAS操作成功,雖然線程1操作成功了,但是這中間經過了線程2,破壞了有序性,屬於線程不安全,從jdk1.5開始引入了AtomicStampedReference
  2. 迴圈時間長開銷大:資源競爭嚴重的情況,CAS自旋的概率會比較大,會浪費更多的CPU資源,效率低於synchronized
  3. 只能保證一個共用變數的原子操作:多個共用變數建議使用鎖

AQS

AQS的全稱是AbstractQueuedSynchronizer,這個類在java.util.concurrent.locks包下麵,是一個用來構建鎖和同步器的框架

核心思想:如果被請求的共用資源空閑,則將當前請求資源的線程設置成有效的工作線程,並且將共用資源設置為鎖定狀態,如果被請求的共用資源被占用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列來實現的,即將暫時獲取不到的鎖加入到隊列中

CLH隊列是一個虛擬的雙向隊列,虛擬的雙向隊列即是一個不存在的隊列實例

基於AQS的實現:ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask

AQS使用一個int成員變數來表示同步狀態,通過內置的FIFO隊列來完成獲取資源線程的排隊工作,AQS使用CAS對該同步狀態進行原子操作實現對其值的修改

private volatile int state;//共用變數,使用volatile修飾保證線程的可見性

//獲取同步狀態的當前值
protected final int getState(){
return state;
}
//設置同步狀態的值
protected final void setState(int newState){
  state = newState
}
//原子操作(CAS操作),將同步狀態值設置為給定值
protected final boolean compareAndSetState(int expect,int update){
 return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}

AQS定義了兩種對資源的共用方式

  1. Exclusive(獨占):只有一個線程能執行,如ReentrantLock,這裡面又可分為公平鎖和非公平鎖
  2. Share(共用):多個線程可同時執行

不同的自定義同步器爭用共用資源的方式也不同,自定義同步器在實現時只需要實現共用資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗時入隊和喚醒出隊等),AQS在頂層已經實現了

AQS使用了模板方法模式,自定義同步器時需要重寫下麵幾個AQS提供的模板方法

isHeldExclusively()//該線程是否正在獨占資源,只有用到condition才需要去實現它
tryAcquire(int) //獨占方式,嘗試獲取資源,成功返回true,失敗返回false
tryRelease(int) //獨占方式,嘗試釋放資源,成功返回true,失敗返回false
tryAcquireShared(int) //共用方式,嘗試獲取資源,負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源;
tryReleaseShared(int) //共用方式,嘗試釋放資源,成功則返回true,失敗返回false

預設情況下每個方法都拋出UnsupportedIOperationException.這些方法的實現必須都是內部線程安全的。並且通常應該簡短而不是阻塞,AQS類中的其他方法都是final,所以無法被其他類使用,只有這幾個方法可以被其他類使用

一般來說,自定義同步器要麼是獨占方式,要麼是共用方式,只需要實現tryAcquire-tryRelease或者tryAcquireShared-tryReleaseShared,但是AQS也支持同時實現兩種方式,比如ReentrantReadWriteLock

鎖實踐

synchronized

三種使用方式

  1. 修飾實例方法:作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
  2. 修飾靜態方法:也就是給當前類加鎖,會作用於類的所有對象實例,因為靜態成員不屬於任何一個實例對象,是類成員
  3. 修飾代碼塊:指定加鎖對象,給它加鎖,進入同步代碼庫前要獲得給定對象的鎖

總結,synchronized關鍵字加到static靜態方法和代碼塊上都是給Class類上鎖。加到實例方法就是給對象實例上加鎖,儘量不要使用到String類型上,因為JVM中字元串常量池具有緩存功能

底層實現原理

public class SynchronizedDemo {
    public void method(){
        synchronized (this){
            System.out.println("synchronized---code");
        }
    }
}

使用javap反編譯後 javap -c -v SynchronizedDemo

image-20230308163553534

在執行方法之前之後都有一個monitorenter和monitorexit字元,前面的monitorenter就是獲取鎖,執行完代碼後釋放鎖,執行monitorexit,第二個monitorexit是為了防止在同步代碼塊中因異常退出而沒有釋放鎖的情況下,第二遍釋放,避免死鎖的情況

可重入的原理

重入的原理是一個線程在獲取到該鎖之後,該線程可以繼續獲得鎖,底層原理維護了一個計數器,當線程獲得該鎖時,計數器加1,再次獲得該鎖時繼續加1,釋放鎖時,計數器減1,當計數器值為0時,表明該鎖未被任何線程所持有,其他線程可以競爭獲取鎖

鎖升級的原理

在鎖對象的對象頭裡面有一個threadId欄位,在第一次訪問的時候threadId為空,jvm讓其持有偏向鎖,並將threadid設置為線程id,再次進入的時候會先判斷threadid是否與其線程id一致,如果一致則可以繼續使用該對象,如果不一致則升級為了輕量級鎖,通過自旋迴圈一定次數之後,如果還沒有正常獲取到要使用的對象,此時就會把鎖再升級為重量級鎖,從而降低鎖帶來的性能消耗

Lock

Lock可以說是Synchronized的擴展版,Lock提供了無條件的,可輪訓的(tryLock方法),定時的(tryLock(long timeout,TimeUnit unit)),可中斷的(lockInterruptitibly),可多條件的(newCondition方法)鎖操作,另外Lock的實現類基本都支持非公平鎖和公平鎖,synchronized只支持非公平鎖

優勢:

  1. 可以使鎖更公平
  2. 可以使線程在等待鎖的時候響應中斷
  3. 可以讓線程嘗試獲取鎖,併在無法獲取鎖的時候立即返回或者等待一段時間
  4. 可以在不同範圍,以不同順序獲取和釋放鎖

主要方法

  1. void lock();如果鎖處於空閑狀態,則當前線程獲取到鎖,如果鎖已經被其他線程持有,則將當前線程阻塞,直到獲取到鎖
  2. boolean tryLock();如果鎖處於可用狀態,則當前線程獲取鎖並且返回true,如果鎖是不可用狀態,則不會將當前線程阻塞
  3. void unlock();釋放當前線程所持有的鎖,這個方法只能由持有者釋放,如果當前線程不說持有者,也就是當前線程不持有鎖,那麼將會拋出異常
  4. Condition newCondition();條件對象,獲取等待通知組件,該組件和當前鎖綁定,當前線程只有獲取到了鎖,才能調用該組件的await方法,await調用後將釋放鎖
  5. getHoldCound():查詢當前線程保存此鎖的次數,也就是執行lock方法的次數
  6. getQueueLength():返回正在等待的獲取此鎖的線程估計數,比如如果是20個線程,獲取到的可能是18個
  7. getWaitQueueLength(Condition condition):返回等待與此鎖相關的給定條件的線程估計數,比如10個線程都用condition對象調用了await方法,那麼此方法返回10
  8. hasWaiters(Condition condition):查詢是否有線程等待與此鎖相關的給定條件
  9. hasQueueThreads(Thread thread):查詢給定線程是否在等待獲取鎖
  10. hasQueuedThreads():是否有線程在等待此鎖
  11. isFair():該鎖是否是公平鎖
  12. isHeldByCurrentThread():當前線程是否保存鎖鎖定,線程執行lock方法的前後分別是false和true
  13. isLock:此鎖是否有任意線程占用
  14. lockInterruptibly():如果當前線程未被中斷,獲取鎖
  15. tryLock():嘗試獲取鎖,僅在調用時鎖未被線程占用,獲取鎖
  16. tryLock(long timeout,TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖

ReentrantLock

ReentrantLock繼承介面Lock並實現了定義的方法,是一種可重入鎖,除了能完成Synchronized所能完成的所有功能外,還提供了諸如可響應的中斷鎖,可輪詢鎖請求,定時鎖等避免死鎖的方法

使用ReentrantLock必須在finally中進行解鎖操作,避免程式出現異常而無法正常解鎖的情況

Synchronized和Lock的區別

synchronized是悲觀鎖,屬於搶占式,會引起其他線程阻塞

Synchronized和ReentrantLock的區別

兩個都是可重入鎖

Synchronized是和if,else,for一樣的關鍵字,ReentrantLock是類

  1. ReentrantLock使用相比Synchronized更加靈活
  2. ReentrantLock必須手動獲取和釋放鎖,Synchronized是自動,由jvm控制
  3. ReentrantLock只適用於代碼塊,Synchronized可以適用於類,方法,變數等
  4. ReentrantLock底層是使用Unsafe的park方法加鎖,synchronized操作是對象頭的monitorenter加鎖

ReentrantLock相比synchronized的優勢是可中斷,公平鎖,多個鎖

Condition類和Object類鎖的區別

  1. Condition類的await方法和Object類的await方法等效
  2. Condition類的signal方法和Object類的notify方法等效
  3. Condition類的signalAll方法和Object類的notifyAll方法等效
  4. ReentranLock類可以喚醒指定條件的的線程,而Object的喚醒是隨機的

tryLock和Lock和lockInterruptibly的區別

  1. tryLock能獲取鎖就返回true,不能就返回false,tryLock(long timeout,TimeUnit unit)增加時間限制,超過該時間還沒獲取到鎖,則返回false
  2. lock能獲取到鎖就返回true,不能的話就會一直等待獲取鎖
  3. lock和lockInterruptibly,如果兩個線程分別執行這兩個方法的時候被中斷,lock不會拋出異常,lockInterruptibly會拋出異常

鎖優化

減少鎖持有時間

只用在有線程安全要求的程式上加鎖

減小鎖粒度

將大對象(這個對象可能會被很多線程訪問),拆成小對象,增加並行度,ConcurrentHashMap就是其中的一個實現

降低鎖競爭

使用偏向鎖,輕量級鎖,降低鎖競爭

鎖分離

根據功能將鎖分離開,一般分為讀鎖和寫鎖,做到讀讀不互斥,讀寫互斥,寫寫互斥,保證了線程安全,又提高了性能

鎖粗化

正常來說是為了保證線程間有效併發,會要求鎖的持有時間儘量短,但是如果時間太短,多個線程對一個鎖不停的請求,同步,釋放,這其中對鎖的開銷也會浪費系統資源

鎖消除

對不需要共用的資源中取消加鎖

容器

容器類存放於Java.util包中,主要有3種:Set(集),list(列表,包含Queue)和Map(映射)

  1. Collection:Collection是集合List,Set,Queue的最基本的介面
  2. Iterator:迭代器,可以通過迭代器遍歷集合中的數據
  3. Map:映射表的基礎介面

Set

  1. HashSet:無序,唯一,基於HashMap實現,底層採用HashMap來保存元素
  2. LinkedHashSet:繼承於HashSet,內部是通過LinkedHashMap來實現
  3. TreeSet:有序,唯一,紅黑樹的數據結構
  4. HashMap:JDK1.8之前由數組+鏈表,鏈表是為瞭解決hash衝突而存在,JDK1.8之後由數組+鏈表+紅黑樹組成,當鏈表長度大於閾值8時,將鏈表轉換為紅黑樹,減少搜索時間
  5. LinkedHashMap:繼承自HashMap,底層是基於數組和鏈表或紅黑樹組成,增加了一個雙向鏈表,使數據可以保持鍵值對的插入順序
  6. HashTable:數組+鏈表組成,數組是HashMap的主體,鏈表主要是為瞭解決哈希衝突而存在
  7. TreeMap:紅黑樹

List

List是有序的Collection,實現有ArrayList,Vector,LinkedList

ArrayList

底層通過數組實現,允許對元素進行快速隨機訪問,缺點是每個元素之間不能有間隔,當容量不夠需要增加容量時,需要將原來的數據複製到新的存儲中間中,當進行插入或者刪除時,需要對數組進行複製,移動,代價比較高

適合隨機查尋和遍歷,不適合插入和刪除,列印時使用Arrays.toString()輸出每個元素

Vector

底層是通過數組實現,是線程安全的,避免了多線程同時寫而引起的不一致性,性能比ArrayList慢

LinkedList

採用雙向鏈表結構存儲數據,很適合數據的動態插入和刪除,隨機訪問和遍歷速度比較慢,提供了專門操作表頭和表尾的元素

Set

set有獨一無二的性質,用於存儲無序(存入和取出)元素,值不能重覆,數據是否重覆的本質是比較對象的HashCode值,所以如果想要讓兩個對象相同,就必須覆蓋Object的hashCode和equals方法,實現有HashSet,TreeSet,LinkHashSet

HashSet

內部採用HashMap實現,不允許重覆的Key,只允許一存儲一個null對象,判斷key是否重覆通過HashCode值來確定

TreeSet

使用二叉樹的結構存儲數據,二叉樹是有序的,排序時需要實現Comparable介面,重寫comoare函數,排序時該函數返回負整數,零,正整數分別對應小於,等於,大於

LinkhashSet

繼承於HashSet,實現了LinkedHashSet,底層採用LinkedHashMap來保存元素

Map

HashMap

不是一個線程安全的容器,預設容量是16,根據鍵的hashCode存儲數據,大多數情況可以根據hash函數一次性獲取到數據,因此具有很快的查詢速度,鍵只允許一個null,值可以有多個null

JDK1.7採用數組+鏈表的方式存儲,每次擴容都是2^n,擴容後是原來的兩倍,負載因數是0.75,擴容的閾值是當前數組容量*負載因數

JDK1.8採用數組+鏈表+紅黑樹方式存儲,相比JDK1.7多了一個紅黑樹,當鏈表的元素超過了8個以後會將鏈表轉換成紅黑樹

ConcurrentHashMap

ConcurrentHashMap是一個線程安全的容器,底層是一個Segment數組,預設長度是16,所以併發數是16,通過繼承ReentrantLock進行加鎖,每次加鎖的時候只鎖住數組中的一個Segment,也就是分段鎖的思想,只要保證了操作的Segment線程安全,也就實現了全局的線程安全

HashTable

HashTable功能和HashMap相識,不同的是他屬於線程安全的,繼承自Dictionary,在性能上不如concurrentHashMap,使用場景較少,因為線程安全的時候使用concurrentHashMap,不需要保證線程安全的時候使用HashMap

TreeMap

TreeMap實現了SortedMap介面,底層數據結構是紅黑樹,保存的時候根據鍵值升序排序,適用於在遍歷的時候需要得到的記錄是排序後的,註意在使用的時候,key必須實現Comparable介面,否則就會拋出運行時異常java.lang.ClassCastException

LinkedHashMap

LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用iterator遍歷的時候,得到的記錄肯定是先插入的,可以在構造時帶參數,控制訪問次序排序

使用場景

  1. 不需要線程安全,對性能要求高使用HashMap
  2. 需要線程安全,使用ConcurrentHashMap
  3. 需要排序,使用treeMap或者LinkedHashMap

Queue

常用隊列

  1. ArrayBlockingQueue:基於數組的併發有界阻塞隊列
  2. ArrayDeque:數組雙端隊列
  3. ConcurrentLinkedQueue:基於鏈表的併發隊列
  4. DelayQueue:使用優先順序排序的延期無界阻塞隊列
  5. LinkedBolckingQueue:基於鏈表的FIFO有界阻塞隊列
  6. LinkedBolckingDeque:基於鏈表的FIFO雙端阻塞隊列
  7. LinkedtransferQueue:由鏈表組成的無界阻塞隊列
  8. PriorityQueue:優先順序隊列
  9. PriorityBlockingQueue:帶優先順序排序的無界阻塞隊列
  10. SynchronousQueue:併發的同步阻塞隊列,不存儲元素

主要方法

  1. add():添加元素到隊列里,成功返回true,如果容量滿了會拋出IllegalStateException
  2. addFirst():插入元素到隊列頭部,失敗拋出異常
  3. addLast():插入元素到隊列尾部,失敗拋出異常
  4. offer():添加元素到隊列里,成功返回true,失敗返回false或拋出異常
  5. offerFirst():添加元素到隊列頭部,成功返回true,失敗返回false或者拋出異常
  6. offerLast():添加元素到隊列尾部,成功返回true,失敗返回false或者拋出異常
  7. remove():取出並移除頭部元素,成功返回true,失敗返回false,空隊列拋出異常
  8. removeFirst():取出並移除頭部元素,空隊列拋出異常
  9. removeLast():取出並移除尾部元素,空隊列拋出異常
  10. poll():取出並刪除隊列頭部元素,如果隊列為空返回null
  11. pollFirst():取出並移除頭部元素,如果隊列 為空返回null
  12. pollLast():取出並刪除尾部元素,如果隊列為空返回null
  13. getFirst():取出但不刪除頭部元素,如果隊列為空拋出異常
  14. getLast():取出但不刪除尾部元素,如果隊列為空拋出異常
  15. peek():取出但不移除頭部元素,空隊列返回null
  16. peekFirst():取出但不刪除頭部元素,空隊列返回null
  17. peekLast():取出但不刪除尾部元素,空隊列返回null
  18. put():添加元素到隊列里,成功返回true,如果容量滿了會阻塞直到容量不滿
  19. take():刪除隊列頭部元素,如果隊列為空,一直阻塞到隊列有元素並刪除
  20. removeFirstOccurrence(Object o):刪除隊列中第一次出現的指定元素,如果不存在則隊列不變,刪除成功返回true
  21. removeLastOccurrence(Object o):刪除隊列中最後一次出現的指定元素,如果不存在則隊列不變,刪除成功返回true

集合和數組的區別

  1. 數組是固定長度,集合是可變長度
  2. 數組可以存儲基本數據類型和引用數據類型;集合只能存儲引用數據類型
  3. 數據存儲的元素必須是同一個類型,集合存儲的數據可以是不同數據類型

線程池理論

原理

利用池化思想,將線程管理起來,使用的時候不需要再創建和銷毀,即用即拿提高了效率,減少了線程

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

-Advertisement-
Play Games
更多相關文章
  • SpringBoot Controller 控制器 SpringBoot提供了@Controller和@RestController兩種註解來標識此類負責接收和處理HTTP請求。 如果請求的是頁面和數據,使用@Controller註解即可;如果只是請求數據,則可以使用@RestController註 ...
  • 1.學習目標 2.簡介 技術論壇:http://bbs.chinaunix.net/forum-240-1.html 資源地址:https://sourceforge.net/projects/fastdfs/ 源碼地址:https://github.com/happyfish100 FastDFS ...
  • 前言 對於大多數 maven 多模塊化工程,可以使用 Jacoco 這款工具,關於 Jacoco 這款工具,ChatGPT 對它的描述是這樣的: JaCoCo(Java Code Coverage)是一個開源的測試覆蓋率工具,它可以用於幫助開發人員衡量其軟體測試的有效性。它支持多種語言,包括 Jav ...
  • 1、Spring 1.1、簡介 Spring:春天 >給軟體行業帶來了春天! 2002,首次推出了Spring框架的雛形:interf21框架! Spring框架即以interface21框架為基礎,經過重新設計,並不斷豐富其內涵,於2004年3月24日發佈了1.0正式版。 Rod Johnson ...
  • 之前給大家寫過如何將 ChatGPT 接入微信和釘釘,沒看過的可以往公眾號前面的文章翻翻,最近又發現了一個有趣的玩法,周末找時間實現了一下,感覺挺不錯的,分享給大家。 背景 事情的起因是阿粉在朋友圈看到了這樣一條信息,敏感信息已經去掉了,意思很明顯就是將 OpenAI 接入到知識星球了,用戶可以通過 ...
  • Celery介紹、安裝、基本使用 一、Celery服務 什麼是Celery: Celery是一個簡單、靈活且可靠的,處理消息的分散式系統 Celery可以用來做什麼: 非同步任務 定時任務 延遲任務 Celery的運行原理: 可以不依賴任何服務,通過自身命令,啟動服務 celery服務為其他項目服務提 ...
  • 問題描述: 利用pyinstaller對python代碼打包後,dist文件夾中會生成一個xxx.exe可執行文件。打包成功,但運行exe時一閃而過(閃退)。捕捉不對到底是打包錯誤呢,還是其他異常?那麼如何解決? PS:以上現象在windows系統中會出現,在Linux和mac系統中不會出現。 解決 ...
  • 將一個正整數n拆分成若幹個正整數的和(至少兩個數,n<=100)。 輸入格式: 一個正整數n 輸出格式: 若幹行,每行一個等式(數與數之間要求非降序排列)。最後一行給出解的總個數 輸入樣例: 在這裡給出一組輸入。例如: 4 輸出樣例: 4=1+1+1+1 4=1+1+2 4=1+3 4=2+2 4 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...