多線程知識點目錄
多線程并發(1)- http://www.lxweimin.com/p/8fcfcac74033
多線程并發(2)-http://www.lxweimin.com/p/a0c5095ad103
多線程并發(3)-http://www.lxweimin.com/p/c5c3bbd42c35
多線程并發(4)-http://www.lxweimin.com/p/e45807a9853e
多線程并發(5)-http://www.lxweimin.com/p/5217588d82ba
多線程并發(6)-http://www.lxweimin.com/p/d7c888a9c03c
七、Java后臺線程(守護線程)
定義:守護線程,也稱“服務線程”,是一種特殊的線程,這種線程不屬于程序中不可或缺的一部分,當沒有用戶線程可服務時,后臺線程會自動離開。
優先級:守護線程的優先級比較低,用來在后臺為系統中其他對象和線程提供服務。
設置:通過setDaemon(true)來設置線程為“守護線程”。將一個用戶線程設置為守護線程的方式是在線程對象創建之前,調用線程對象的setDaemon()方法。
在Daemon線程中產生的新線程也是Daemon線程。
線程是JVM級別的,獨立于具體的Java應用程序,生命周期是由操作系統來管理。以Tomcat為例,它在Web應用中啟動的線程并不會與Web應用保持同步的生命周期。即使你停止了Web應用,這個線程仍然會繼續運行。
守護線程示例:垃圾回收線程。當程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器就無事可做,*所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用于實時監控和管理系統中的可回收資源。
生命周期:守護進程(Daemon)是運行在后臺的一種特殊進程,它不與用戶進行交互,也不依賴于終端。守護線程不會受到用戶終端的控制,它會按照一定的周期或條件執行特定的任務,或者等待處理某些事件。守護線程不依賴于用戶終端,但它依賴于操作系統。當系統運行時,守護線程會一直存在并執行任務,當系統關閉時,守護線程也會終止。如果JVM中的所有線程都是守護線程,那么JVM會認為沒有需要繼續運行的線程,因此可以退出。但如果還有非守護線程在運行,那么JVM會認為還有其他工作需要完成,因此不會退出。(簡單來說:守護線程是一種在后臺運行并執行特定任務的特殊線程。它是獨立于用戶終端的,但依賴于操作系統。當系統中沒有非守護線程時,JVM會選擇退出)
八、JAVA鎖
8.1 樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,拿取數據時認為別人不會修改,所以不上鎖,但是在更新的時候會判斷一下在次期間別人有沒有去更新這個數據,采取先讀出當前版本號,然后加鎖操作,比較跟上一次的版本號,一樣則更新,如果失敗則重讀-比較-寫操作。
Java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否相同,同則更新,否則失敗。
8.2 悲觀鎖
悲觀鎖是一種悲觀思想,即認為寫多,遇到并發寫的可能性高,拿取數據時認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。
Java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試CAS樂觀鎖去獲取鎖,獲取不到才會轉換為悲觀鎖,如RetreenLock。
8.3 自旋鎖
自旋鎖原理:如果持有鎖的線程能再很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等待有鎖的線程釋放鎖后立即獲取,就避免用戶線程和內核的切換的消耗。
線程自旋是需要消耗CPU的,如果一直獲取不到鎖,那線程也不能一直占用CPU自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間仍沒有釋放鎖,則會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋,進入阻塞狀態。
8.4 Synchronized同步鎖
synchronized可以把任意一個非NULL的對象當作鎖。它屬于獨占式的悲觀鎖,同時屬于可重入鎖。
synchronized作用范圍
- 作用于方法時,鎖住的是對象的實例(this)
- 作用于靜態方法時,鎖住的是Class實例,又因為Class的相關數據存儲在永久帶PermGen(JDK1.8則是metaspace),永久帶是全局共享的,因此靜態方法鎖相當于類的一個全局鎖,會鎖所有調用該方法的線程
- 作用于一個對象實例時,鎖住的是所有以該對象為鎖的代碼塊。它有過個隊列,當多個線程一起訪問某個對象監視器時,對象監視器會將這些線程存儲在不同的容器中。
synchronized核心組件
- Wait Set(等待集合):那些調用wait方法被阻塞的線程被放置在這里
- Contention List(鎖競爭隊列):競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中
- Entry List(競爭候選列表):Contention List中那些有資格成為候選資源的線程被移動到Entry List中
- OnDeck(待處理隊列):任意時刻,最多只有一個線程正在競爭鎖資源,該線程被稱為OnDeck
- Owner(持鎖狀態):當前已經獲取到所有資源的線程稱為Owner
- !Owner:當前釋放鎖的線程
synchronized實現機制
8.5 ReentrantLock
ReentrantLock繼承接口Lock并實現了接口中定義的方法,他是一種可重入鎖,除了能完成synchronized所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。
ReentrantLock特性
可重入:意味著一個線程可以多次獲取同一個鎖,而不會產生死鎖。
公平鎖:ReentrantLock 可以配置為公平鎖和非公平鎖。公平鎖按照線程請求鎖的順序來分配鎖,而非公平鎖則沒有這個限制。
可中斷:當一個線程持有鎖時,其他線程可以嘗試獲取鎖,如果獲取失敗,那么這個線程可以選擇中斷等待的線程。
可嘗試:可以嘗試獲取鎖,而不會阻塞當前線程。
ReentrantLock使用
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void accessResource() {
// 獲取鎖
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
// 釋放鎖
lock.unlock()
}
}
}
公平鎖 & 非公平鎖
公平鎖:通常先對鎖提出獲取請求的線程會先被分配到鎖。
非公平鎖:JVM按隨機、就近原則分配鎖的機制。
效率:非公平鎖執行的效率要遠遠超出公平鎖,除非程序有特殊需求,否則最常用非公平鎖。
可以通過在創建 ReentrantLock 時設置參數來選擇使用公平鎖還是非公平鎖,默認為非公平鎖。
// 公平鎖 - 鎖會盡可能地按照線程請求的順序分配
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平鎖
ReentrantLock unfairLock = new ReentrantLock(false);
ReentrantLock 與 Synchronized
- ReentrantLock通過方法lock()與unlock()來進行加鎖與解鎖操作,與Synchronized會被JVM自動解鎖機制不同,ReentrantLock加鎖后需要手動進行解鎖。為了避免程序出現異常而無法正常解鎖的情況,使用ReentrantLock必須在finally控制塊中進行解鎖操作。
- ReentrantLock相比Synchronized的優勢是可中斷、公平鎖、多個鎖。這種情況下需要使用ReentrantLock。
8.6 Senaphore信號量
Semaphore是一種基于計數的信號量。它可以設定一個閥值,基于此,多個線程競爭獲取許可信號,做完自己的申請后歸還,超過閥值后,線程申請許可信號將會被阻塞。Semaphore可用來構建一些對象池、資源池之類的,比如數據庫連接池。
實現互斥鎖(計數器為1):創建計數為1的Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static Semaphore semaphore = new Semaphore(3); // 允許3個線程同時訪問共享資源
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 獲取一個許可證,如果沒有,線程將阻塞直到有一個可用
System.out.println("Thread " + Thread.currentThread().getId() + " is accessing the resource.");
Thread.sleep(1000); // 模擬資源訪問時間
System.out.println("Thread " + Thread.currentThread().getId() + " finished accessing the resource.");
semaphore.release(); // 釋放許可證,允許其他線程獲取許可證并訪問資源
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
在這個例子中,我們創建了一個信號量 semaphore,初始化了3個許可證。然后我們創建了5個線程,每個線程都會嘗試獲取許可證來訪問共享資源。如果當前沒有可用的許可證,線程將會被阻塞直到有一個可用。當線程訪問完共享資源后,它會釋放一個許可證,這樣其他線程就可以獲取許可證并訪問資源。
Semaphore 與 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也類似,通過acquire()與release()方法來獲得和釋放臨界資源。
經實測,Semaphore.acquire()方法默認為可響應中斷鎖,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名TryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。
Semaphore 的鎖釋放操作也由手動進行,因此與 ReentrantLock 一樣,為避免線程因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 代碼塊中完成。
8.7 AtomicInteger
AtomicInteger是一個提供原子操作的Integer的類,常見的還有AtomicBoolean、AtomicLong、AtomicReference等,他們的實現原理相同,區別在與運算對象類型不同。還可以通過AtomicReference<T>將一個對象的所有操作轉化成原子操作。
在多線程程序中,諸如++i或i++等運算不具有原子性,是不安全的線程操作之一。通常我們會使用Synchronized將操作變成一個原子操作,但JVM為此類操作特意提供了一些同步類,使得使用更方便,且使程序運行效率更高。
8.8 ReadWriteLock讀寫鎖
Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制。
如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率。多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由JVM控制的,無需我們來控制。
九、線程上下文切換
時間片輪轉(或稱為時間片調度)是一種策略,CPU 依次為每個任務提供一段時間的服務,然后把當前任務的狀態保存下來,在加載下一任務的狀態后,繼續服務下一任務,任務的狀態保存及再加載, 這段過程就叫做上下文切換。時間片輪轉的方式使多個任務在同一顆 CPU 上執行變成了可能。
1)進程
是指一個程序運行的實例(又稱任務)。在Linux系統中,線程就是能并行運行并且與他們的父進程(創建他們的進程)共享同一地址空間(一段內存區域)和其他資源的輕量級的進程。
2)上下文
指某一時間點CPU寄存器和程序計數器的內容。
3)寄存器
CPU內部的數量較少但速度很快的內存(與之對應的是CPU外部相對較慢的RAM主內存)。寄存器通過對常用值的快速訪問來提高計算機程序運行的速度。
4)程序計數器
一個專用的寄存器,用于表明指令序列中CPU正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴于特定的系統。
5)PCB-“切換幀”
上下文切換可以認為是內核在CPU上對進程進行切換,上下文切換過程中的信息是保存在進程控制塊(PCB, Process Control Block)中的。PCB還經常被稱作“切換幀”。信息會一直保存到CPU的內存中,知道他們被再次使用。
6)上下文切換的活動
- 掛起一個進程,將這個進程在CPU中的狀態(上下文)存儲于內存中的某處。
- 在內存中檢索下一個進程的上下文并將其在CPU的寄存器中回復。
- 跳轉到程序計數器所指向的位置(即進程被中斷時的代碼行),以恢復該進程在程序中。
7)引起上下文切換的原因
- 當前執行任務的時間片用完之后,系統CPU正常調度下一個任務;
- 當前執行任務碰到IO阻塞,調度器將此任務掛起,繼續下一任務;
- 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
- 用戶代碼掛起當前任務,讓出CPU時間;
- 硬件終端。
十、線程池原理
線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然后在線程創建后啟動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等待,等其他線程執行完畢,再從隊列中取出任務來執行。它的主要特點是:線程復用、控制最大并發數、管理線程。
10.1 線程復用
每一個Thread的類都有一個start()方法。當調用start()啟動線程時Java虛擬機會調用該類的run()方法。那么該類的run()方法中就是調用了Runnable對象的run()方法。我們可以繼承重寫Thread類,再其start()方法中添加不斷循環調用傳遞過來的Runnable對象。這就是線程池的實現原理。循環方法中不斷獲取Runnable是用Queue實現的,在獲取下一個Runnable之前可以是阻塞的。
10.2 線程池的組成
- 線程池管理器:用于創建并管理線程池
- 工作線程:線程池中的線程
- 任務接口:每個任務必須實現的接口,用于工作線程調度其運行
- 任務隊列:用于存放待處理的任務,提供一種緩沖機制
Java中的線程池是通過Executor框架實現的,該框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable和Future、FutureTask這幾個類。
10.3 拒絕策略
線程池中的線程已經用完了,無法繼續為新任務服務,同時,等待隊列也已經排滿了,再也塞不下新任務了。這時候,我們就需要拒絕策略機制合理的處理這個問題。
JDK內置的拒絕策略:
- AbortPolicy:直接拋出異常,組織系統正常運行。
- CallerRunsPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。
- DiscardOldestPolicy:丟棄最老的一個請求,也就是即將被執行的一個任務,并嘗試再次提交當前任務。
- DiscardPolicy:丟棄無法處理的任務,不予任何處理。如果運行任務丟失,這是最好的一種方案。
以上內置拒絕策略均實現了RejectedExecutionHandler接口,若以上策略仍無法滿足實際需要,可拓展RejectedExecutionHandler接口。
10.4 Java線程池工作過程
- 線程池剛創建時,里面沒有線程。任務隊列是作為參數傳進來的。不過,就算隊列里面有任務,線程池也不會馬上執行它們。
- 當調用execute()方法添加一個任務時,線程池會做如下判斷:
- (1)如果正在運行的線程數量小于corePoolSize,那么馬上創建線程運行這個任務;
- (2)如果正在運行的線程數大于或等于corePoolSize,那么將這個任務放入隊列。
- (3)如果這時候隊列滿了,而且正在運行的線程數量小于maximimPoolSize,那么還是要創建非核心線程立即運行這個任務。
- (4)如果隊列滿了,而且正在運行的線程數量大于或等于maximumPoolSize,那么線程池會拋出異常RejectExecutionExeption。
- 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
- 當一個線程無事可做,超過一定時間(KeepAliveTime)時,線程池會判斷,如果當前運行的線程大于CorePoolSize,那么這個線程就被停掉。所以線程池的所有任務完成后,它最終會縮到CorePoolSize的大小。