多線程和併發是求職大小廠面試中必問的知識點,其涉及到點很多,難度很大。有些人面對這些問題有點迷茫,爲了解決這情況,總結了一下java多線程併發的基礎知識點。而且要想深入研究java多線程併發也必須先掌握基礎知識,可爲後續各個模塊深入研究做好做好準備。現在廢話不多說,各位看官請查看基礎知識點,後續還有源碼解析(
synchronize
底層原理,線程池原理,Lock
,AQS
,同步、併發容器等源碼解析)。
1 基本概念
程序: 是計算機指令的集合,它以文件的形式存儲在磁盤上,即程序是靜態的代碼
進程:
- 是一個程序在其自身的地址空間中的一次執行活動,是系統運行程序的基本單位
- 進程是資源申請、調度和獨立運行的單位
線程:
- 是進程中的一個單一的連續控制流程。一個進程可以擁有多個線程。
- 線程又稱爲輕量級進程,它和進程一樣擁有獨立的執行控制,由操作系統負責調度,區
別在於線程沒有獨立的存儲空間,而是和所屬進程中的其它線程共享一個存儲空間,這
使得線程間的通信遠較進程簡單。
三者之間的關係:
- 線程是進程劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因爲同一進程中的線程極有可能會相互影響。
- 從另一角度來說,進程屬於操作系統的範疇,主要是同一段時間內,可以同時執行一個以上的程序,而線程則是在同一程序內幾乎同時執行一個以上的程序段。
內存機制可查看文章《推薦收藏系列:一文理解JVM虛擬機(內存、垃圾回收、性能優化)解決面試中遇到問題》
2 線程組成
組成部分:虛擬CPU、執行的代碼以及處理的數據。
3 線程與進程區別
進程: 指系統中正在運行中的應用程序,它擁有自己獨立的內存空間;
線程: 是指進程中一個執行流程,一個進程中允許同時啓動多個線程,他們分別執行不同的任務,多個線程共享內存,從而極大地提高了程序的運行效率;
主要區別:
- 每個進程都需要操作系統爲其分配獨立的內存地址空間
- 而同一進程中的所有線程在同一塊地址空間中,這些線程可以共享數
據,因此線程間的通信比較簡單,消耗的系統開銷也相對較小
4 爲什麼要使用多線程
使用多線程好處:
- 可以同時併發執行多個任務
- 程序的某個功能部分正在等待某些資源的時候,此時又不願意因爲等待而造成程序暫停,那麼就可以創建另外的線程進行其它的工作;
- 多線程可以最大限度地減低CPU的閒置時間,從而提高CPU的利用率;
5 主線程
Java程序啓動時,一個線程立刻運行,它執行main方法,這個線程稱爲程序的主線程,任何Java程序都至少有一個線程,即主線程。
主線程的特殊之處在於:
- 它是產生其它線程子線程的線程;
- 通常它必須最後結束,因爲它要執行其它子線程的關閉工作。
6 線程優先級
單核計算機只有一個CPU,各個線程輪流獲得CPU的使用權,才能執行任務:
- 優先級較高的線程有更多獲得CPU的機會,反之亦然;
- 優先級用整數表示,取值範圍是1~10,一般情況下,線程的默認
- 優先級都是5,但是也可以通過setPriority和getPriority方法來設置或返回優先級;
Thread
類有如下3個靜態常量來表示優先級:
- MAX_PRIORITY:取值爲10,表示最高優先級
- MIN_PRIORITY:取值爲1,表示最低優先級
- NORM_PRIORITY:取值爲5,表示默認的優先級
7 線程的生命週期
線程狀態(State
枚舉值代表線程狀態):
- 新建狀態( NEW): 線程剛創建, 尚未啓動。
Thread thread = new Thread()
。 - 可運行狀態(RUNNABLE): 線程對象創建後,其他線程(比如 main 線程)調用了該對象的
start
方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取 cpu 的使用權。 - 運行(running): 線程獲得 CPU 資源正在執行任務(
run()
方法),此時除非此線程自動放棄 CPU 資源或者有優先級更高的線程進入,線程將一直運行到結束 - 阻塞狀態(Blocked): 線程正在運行的時候,被暫停,通常是爲了等待某個時間的發生(比如說某項資源就緒)之後再繼續運行。
sleep
,suspend
,wait
等方法都可以導致線程阻塞 - 等待(WAITING): 進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
- 超時等待(TIMED_WAITING): 該狀態不同於
WAITING
,它可以在指定的時間後自行返回。 - 終止(TERMINATED): 表示該線程已經執行完畢,如果一個線程的
run
方法執行結束或者調用stop
方法後,該線程就會死亡。對於已經死亡的線程,無法再使用start
方法令其進入就緒。
線程在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)。
8 線程創建方式
線程創建方式:
- 實現Runnable接口,重載
run()
,無返回值 - 繼承Thread類,複寫
run()
- 實現Callable接口,通過FutureTask/Future來創建有返回值的Thread線程,通過Executor執行
- 使用Executors創建ExecutorService,入參Callable或Future
1.實現Runnable接口,重載run()
,無返回值,Runnable接口的存在主要是爲了解決Java中不允許多繼承的問題。
public class ThreadRunnable implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadMain {
public static void main(String[] args) throws Exception {
ThreadRunnable threadRunnable1 = new ThreadRunnable();
ThreadRunnable threadRunnable2 = new ThreadRunnable();
ThreadRunnable threadRunnable3 = new ThreadRunnable();
Thread thread1 = new Thread(threadRunnable1);
Thread thread2 = new Thread(threadRunnable2);
Thread thread3 = new Thread(threadRunnable3);
thread1.start();
thread2.start();
thread3.start();
}
}
2.繼承Thread類,重寫run()
,通過調用Thread的start()
會調用創建線程的run()
,不同線程的run方法裏面的代碼交替執行。但由於Java不支持多繼承.因此繼承Thread類就代表這個子類不能繼承其他類.
public class ThreadCustom extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
}
public class ThreadTest {
public static void main(String[] args)
{
ThreadCustom thread = new ThreadCustom();
thread.start();
}
}
3.實現Callable接口,通過FutureTask/Future來創建有返回值的Thread線程,通過Executor執行,該方式有返回值,可以獲得異步。
public class ThreadCallableCustom {
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return 1;
}
});
Executor executor = Executors.newFixedThreadPool(1);
((ExecutorService) executor).submit(futureTask);
//獲得線程執行狀態
System.out.println(Thread.currentThread().getName() + ":" + futureTask.get());
}
}
4.使用Executors創建ExecutorService,入參Callable或Future,適用於線程池和併發
public class ThreadExecutors {
private final String threadName;
public ThreadExecutors(String threadName) {
this.threadName = threadName;
}
private ThreadFactory createThread() {
ThreadFactory tf = new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread thread = new Thread();
thread.setName(threadName);
thread.setDaemon(true);
try {
sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
return thread;
}
};
return tf;
}
public Object runCallable(Callable callable) {
return Executors.newSingleThreadExecutor(createThread()).submit(callable);
}
public Object runFunture(Runnable runnable) {
return Executors.newSingleThreadExecutor(createThread()).submit(runnable);
}
}
public class ThreadTest {
public static void main(String[] args) throws Exception {
ThreadExecutors threadExecutors = new ThreadExecutors("callableThread");
threadExecutors.runCallable(new Callable() {
public String call() throws Exception {
return "success";
}
});
threadExecutors.runFunture(new Runnable() {
public void run() {
System.out.println("execute runnable thread.");
}
});
}
}
9 Runnable接口和Callable接口區別
1)兩個接口需要實現的方法名不一樣,Runnable需要實現的方法爲run()
,Callable需要實現的方法爲call()
。
2)實現的方法返回值不一樣,Runnable任務執行後無返回值,Callable任務執行後可以得到異步計算的結果。
3)拋出異常不一樣,Runnable不可以拋出異常,Callable可以拋出異常。
10 線程安全
線程安全定義
當多個線程訪問某個一類(對象或方法)時,這個類始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的(即在多線程環境中被調用時,能夠正確地處理多個線程之間的共享變量,使程序功能正確完成)。
線程安全示例
餓漢式單例模式-線程安全
public class EagerSingleton(){
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton(){};
public static EagerSingleton getInstance(){
return instance;
}
}
如何解決線程安全問題?
可以通過加鎖的方式:
- 同步(synchronized)代碼塊:只需要將操作共享數據的代碼放在synchronized
- 同步(synchronized)方法:將操作共享數據的代碼抽取出來放到一個synchronized方法裏面就可以了
- Lock鎖:加同步鎖
lock()
以及釋放同步鎖unlock()
11 什麼是死鎖、活鎖?
死鎖,是指兩個或兩個以上的進程(或線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。
活鎖,任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。
產生死鎖的必要條件:
- 互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
死鎖的解決方法:
- 撤消陷於死鎖的全部進程。
- 逐個撤消陷於死鎖的進程,直到死鎖不存在。
- 從陷於死鎖的進程中逐個強迫放棄所佔用的資源,直至死鎖消失。
從另外一些進程那裏強行剝奪足夠數量的資源分配給死鎖進程,以解除死鎖狀態。
12 什麼是悲觀鎖、樂觀鎖?
1)悲觀鎖
悲觀鎖,總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
- 傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
- Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
2)樂觀鎖
樂觀鎖,顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量。
13 多個線程間鎖的併發控制
多個線程間鎖的併發控制,對象鎖多個線程、每個線程持有該方法所屬對象的鎖以及類鎖。synchronized, wait, notify 是任何對象都具有的同步工具
對象鎖的同步和異步
- 同步:synchronized,同步的概念就是共享,只需要針對共享的資源,才需要考慮同步。
- 異步:asynchronized,異步的概念就是獨立,相互之間不受到任何制約。
同步的目的就是爲線程安全,其實對於線程安全來說,需要滿足兩個特性:原子性(同步)、可見性。
14 Volatile關鍵字
Volatile作用,實現變量在多個線程間可見,保證內存可見性和禁止指令重排
多線程的內存模型:main memory(主存)、working
memory(線程棧),在處理數據時,線程會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變量的操作都激發一次load and save)。
15 ThreadLocal
線程局部變量,以空間換時間的手段,爲每個線程提供變量的獨立副本,以無鎖的情況下保障線程安全。主要解決的就是讓每個線程執行完成之後再結束,這個時候就要用到join()方法。
適用場景:
- 在併發不是很高的時候,加鎖的性能會更好
- 在高併發量場景下,使用ThreadLocal可以在一定程度上減少鎖競爭。
16 多線程同步和互斥實現方法
1). 線程同步,是指線程之間所具有的一種制約關係,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。
線程間的同步方法,大體可分爲兩類:用戶模式和內核模式。顧名思義:
內核模式,就是指利用系統內核對象的單一性來進行同步,使用時需要切換內核態與用戶態。內核模式下的方法有:
- 事件
- 信號量
- 互斥量
用戶模式,就是不需要切換到內核態,只在用戶態完成操作。用戶模式下的方法有:
- 原子操作(例如一個單一的全局變量)
- 臨界區
2). 線程互斥,是指對於共享的進程系統資源,在各單個線程訪問時的排它性。
當有若干個線程都要使用某一共享資源時,任何時刻最多隻允許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。
線程互斥可以看成是一種特殊的線程同步。
17 線程之間通信
線程是操作系統中獨立的個體,但這些個體之間如果不經過特殊的協作就不能成爲一個整體,線程間的通信就成爲整體的必用方式之一。
線程間通信的幾種方式?
線程之間的通信方式:
- 共享內存
- 消息傳遞
共享內存:在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。典型的共享內存通信方式,就是通過共享對象進行通信。
消息傳遞:在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信。在 Java 中典型的消息傳遞方式,就是 wait()
和 notify()
,或者 BlockingQueue 。
18 什麼是 Java Lock 接口?
java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展行的鎖操作。它允許更靈活的結構,可以具有完全不同的性質,並且可以支持多個相關類的條件對象。它的優勢有:
- 可以使鎖更公平。
- 可以使線程在等待鎖的時候響應中斷。
- 可以讓線程嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間。
- 可以在不同的範圍,以不同的順序獲取和釋放鎖。
19 Java AQS
AQS ,AbstractQueuedSynchronizer ,即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 併發包的作者(Doug Lea)期望它能夠成爲實現大部分同步需求的基礎。它是 J.U.C 併發包中的核心基礎組件。
優勢:
AQS 解決了在實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO 同步隊列。基於 AQS 來構建同步器可以帶來很多好處。它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。
在基於 AQS 構建的同步器中,只能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高了吞吐量。同時在設計 AQS 時充分考慮了可伸縮性,因此 J.U.C 中,所有基於 AQS 構建的同步器均可以獲得這個優勢。
20 同步類容器
何爲同步容器?可以簡單地理解爲通過synchronized來實現同步的容器,如果有多個線程調用同步容器的方法,它們將會串行執行。
特點:
- 是線程安全的
- 某些場景下可能需要加鎖來保護複合操作
常見同步類容器:
- 如Vector、HashTable
- 使用JDK的Collections.synchronized等工廠方法去創建實現的。
- 底層是用傳統的synchronized關鍵字對方法進行同步。
- 無法滿足高併發場景下的性能需求
21 併發類容器
jdk5.0以後提供了多種併發類容器來替代同步類容器從而改善性能。
同步類容器侷限性:
- 都是串行化的。
- 雖實現了線程安全,卻降低了併發性
- 在多線程環境時,嚴重降低了應用程序的吞吐量。
常用的併發類容器:
- ConcurrentHashMap
- Copy-On-Write容器
ConcurrentHashMap原理
- 內部使用段(Segment)來表示這些不同的部分
- 每個段相當於一個小的HashTable,它們有自己的鎖。
- 把一個整體分成了16個段(Segment)。也就是最高支持16個線程的併發修改操作。
- 這也是在多線程場景時通過減小鎖的粒度從而降低鎖競爭的一種方案。
Copy-On-Write容器
Copy-On-Write簡稱COW,是一種用於程序設計中的優化策略。
- 讀寫分離,讀和寫分開
- 最終一致性
- 使用另外開闢空間的思路,來解決併發衝突
JDK裏的COW容器有兩種:
- CopyOnWriteArrayList:適用於讀操作遠遠多於寫操作的場景,例如,緩存.
- CopyOnWriteArraySet:線程安全的無序的集合,可以將它理解成線程安全的HashSet,適用於Set 大小通常保持很小,只讀操作遠多於可變操作,需要在遍歷期間防止線程間的衝突
22 併發Queue
併發Queue:
- ConcurrentLinkedQueue,高性能隊列,當許多線程共享訪問一個公共集合時,ConcurrentLinkedQueue 是一個恰當的選擇。
- BlockingQueue,阻塞隊列,是一個支持兩個附加操作的隊列,常用於生產者和消費者的場景。
ConcurrentLinkedQueue
- 適用於高併發場景
- 使用無鎖的方式,實現了高併發狀態下的高性能
- 其性能好於BlockingQueue
- 遵循先進先出的原則
常用方法:
- Add()和offer()都是加入元素的方法
- Poll()和peek()都是取頭元素節點,區別在於前者會刪除元素,後者不會。
BlockingQueue接口實現:
- ArrayBlockingQueue:基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長的數組,以便緩存隊列中的數據對象,其內部沒實現讀寫分離,也就意味着生產和消費者不能完全並行,適用很多場景。
- LinkedBlockingQueue:基於鏈表的阻塞隊列,同ArrayBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),LinkedBlockingQueue之所以能夠高效地處理併發數據,是因爲其內部實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操作完全並行運行。
- PriorityBlockingQueue:基於優先級別的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定,也就是說傳入隊列的對象必須實現Comparable接口),在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖
- DelayQueue:帶有延遲時間的Queue,其中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue中的元素必須先實現Delayed接口,DelayQueue是一個沒有大小限制的隊列,應用場景很多,比如對緩存超時的數據進行移除、任務超時處理、空閒連接的關閉等等。
- SynchronousQueue:一種沒有緩衝的隊列,生產者產生的數據直接會被消費者獲取並且立刻消費
23 多線程的設計模式
- 基於並行設計模式演變而來
- 屬於設計優化的一部分
- 是對一些常用的多線程結構的總結和抽象
- 常見的多線程設計模式有哪些?
24 Concurrent.util常用類
CountDownLatch: 用於監聽某些初始化操作,等初始化執行完畢後,通知主線程繼續工作。
CycilcBarrier: 所有線程都準備好後,才一起出發,只要有一個人沒有準備好,大家都等待。
Concurrent.util常用類
定義:實現異步回調,jdk針對該場景提供了一個實現的封裝,簡化了調用
適合場景:處理耗時的業務邏輯時,可有效的減少系統的響應時間,提高系統的吞吐量。
Concurrent.util常用類
Semaphore:信號量,適合高併發訪問, 用於進行訪問流量的控制
ReentrantLock(重入鎖)
重入鎖,在需要進行同步的代碼部分加上鎖定,但不要忘記最後一定要釋放鎖定,不然會造成鎖永遠無法釋放,其他線程永遠也進不來的結果。
鎖與等待/通知
- 多線程間進行協作工作則需要Object的wait()和notify()、notifyAll()方法進行配合工作
- 在使用鎖的時候,可以使用一個新的等待/通知的類,它就是Condition
- 這個Condition是針對具體某一把鎖的
多Condition
- 可通過一個Lock對象產生多個Condition進行多線程間的交互
- 使得部分需要喚醒的線程喚醒,其他線程則繼續等待通知。
ReentrantReadWriteLock(讀寫鎖)
- 其核心就是實現讀寫分離的鎖。尤其適應在在高併發訪問下讀多寫少的情況下,性能要遠高於重入鎖。
- 將讀寫鎖分離爲讀鎖和寫鎖
- 在讀鎖下,多個線程可以併發進行訪問
- 在寫鎖下,只能一個一個的順序訪問
- 鎖優化
25 線程池
使用 Executor 框架的原因:
- 每次執行任務創建線程 new Thread() 比較消耗性能,創建一個線程是比較耗時、耗資源的。
- 調用 new Thread() 創建的線程缺乏管理,被稱爲野線程,而且可以無限制的創建,線程之間的相互競爭會導致過多佔用系統資源而導致系統癱瘓,還有線程之間的頻繁交替也會消耗很多系統資源。
- 接使用 new Thread() 啓動的線程不利於擴展,比如定時執行、定期執行、定時定期執行、線程中斷等都不便實現
線程池的創建方式:
普通任務線程池
-
newFixedThreadPool(int nThreads) 方法,創建一個固定長度的線程池。
- 每當提交一個任務就創建一個線程,直到達到線程池的最大數量,這時線程規模將不再變化。
- 當線程發生未預期的錯誤而結束時,線程池會補充一個新的線程。
-
newCachedThreadPool() 方法,創建一個可緩存的線程池。
- 如果線程池的規模超過了處理需求,將自動回收空閒線程。
- 當需求增加時,則可以自動添加新線程。線程池的規模不存在任何限制。
-
newSingleThreadExecutor() 方法,創建一個單線程的線程池。
- 它創建單個工作線程來執行任務,如果這個線程異常結束,會創建一個新的來替代它。
- 它的特點是,能確保依照任務在隊列中的順序來串行執行。
定時任務線程池
- newScheduledThreadPool(int corePoolSize) 方法,創建了一個固定長度的線程池,而且以延遲或定時的方式來執行任務,類似 Timer 。
- newSingleThreadExecutor() 方法,創建了一個固定長度爲 1 的線程池,而且以延遲或定時的方式來執行任務,類似 Timer 。
線程池的關閉方式
ThreadPoolExecutor 提供了兩個方法,用於線程池的關閉,分別是:
- shutdown() 方法,不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務。
- shutdownNow() 方法,立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務