內容摘自我的學習網站:topjavaer.cn 分享50道Java併發高頻面試題。 線程池 線程池:一個管理線程的池子。 為什麼平時都是使用線程池創建線程,直接new一個線程不好嗎? 嗯,手動創建線程有兩個缺點 不受控風險 頻繁創建開銷大 為什麼不受控? 系統資源有限,每個人針對不同業務都可以手動創 ...
內容摘自我的學習網站:topjavaer.cn
分享50道Java併發高頻面試題。
線程池
線程池:一個管理線程的池子。
為什麼平時都是使用線程池創建線程,直接new一個線程不好嗎?
嗯,手動創建線程有兩個缺點
- 不受控風險
- 頻繁創建開銷大
為什麼不受控?
系統資源有限,每個人針對不同業務都可以手動創建線程,並且創建線程沒有統一標準,比如創建的線程有沒有名字等。當系統運行起來,所有線程都在搶占資源,毫無規則,混亂場面可想而知,不好管控。最全面的Java面試網站
頻繁手動創建線程為什麼開銷會大?跟new Object() 有什麼差別?
雖然Java中萬物皆對象,但是new Thread() 創建一個線程和 new Object()還是有區別的。
new Object()過程如下:
- JVM分配一塊記憶體 M
- 在記憶體 M 上初始化該對象
- 將記憶體 M 的地址賦值給引用變數 obj
創建線程的過程如下:
- JVM為一個線程棧分配記憶體,該棧為每個線程方法調用保存一個棧幀
- 每一棧幀由一個局部變數數組、返回值、操作數堆棧和常量池組成
- 每個線程獲得一個程式計數器,用於記錄當前虛擬機正在執行的線程指令地址
- 系統創建一個與Java線程對應的本機線程
- 將與線程相關的描述符添加到JVM內部數據結構中
- 線程共用堆和方法區域
創建一個線程大概需要1M左右的空間(Java8,機器規格2c8G)。可見,頻繁手動創建/銷毀線程的代價是非常大的。
為什麼使用線程池?
- 降低資源消耗。通過重覆利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度。當任務到達時,可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性。統一管理線程,避免系統創建大量同類線程而導致消耗完記憶體。
線程池執行原理?
- 當線程池裡存活的線程數小於核心線程數
corePoolSize
時,這時對於一個新提交的任務,線程池會創建一個線程去處理任務。當線程池裡面存活的線程數小於等於核心線程數corePoolSize
時,線程池裡面的線程會一直存活著,就算空閑時間超過了keepAliveTime
,線程也不會被銷毀,而是一直阻塞在那裡一直等待任務隊列的任務來執行。 - 當線程池裡面存活的線程數已經等於corePoolSize了,這是對於一個新提交的任務,會被放進任務隊列workQueue排隊等待執行。
- 當線程池裡面存活的線程數已經等於
corePoolSize
了,並且任務隊列也滿了,假設maximumPoolSize>corePoolSize
,這時如果再來新的任務,線程池就會繼續創建新的線程來處理新的任務,知道線程數達到maximumPoolSize
,就不會再創建了。 - 如果當前的線程數達到了
maximumPoolSize
,並且任務隊列也滿了,如果還有新的任務過來,那就直接採用拒絕策略進行處理。預設的拒絕策略是拋出一個RejectedExecutionException異常。
本文已經收錄到大彬精心整理的大廠面試手冊,手冊包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等高頻面試題,非常實用,有小伙伴靠著這份手冊拿過位元組offer~
需要的小伙伴可以自行下載:
線程池參數有哪些?
ThreadPoolExecutor 的通用構造函數:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
1、corePoolSize
:當有新任務時,如果線程池中線程數沒有達到線程池的基本大小,則會創建新的線程執行任務,否則將任務放入阻塞隊列。當線程池中存活的線程數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
2、maximumPoolSize
:當阻塞隊列填滿時,如果線程池中線程數沒有超過最大線程數,則會創建新的線程運行任務。否則根據拒絕策略處理新任務。非核心線程類似於臨時借來的資源,這些線程在空閑時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
3、BlockingQueue
:存儲等待運行的任務。
4、keepAliveTime
:非核心線程空閑後,保持存活的時間,此參數只對非核心線程有效。設置為0,表示多餘的空閑線程會被立即終止。
5、TimeUnit
:時間單位
TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS
6、ThreadFactory
:每當線程池創建一個新的線程時,都是通過線程工廠方法來完成的。在 ThreadFactory 中只定義了一個方法 newThread,每當線程池需要創建新線程就會調用它。
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);//將線程池名字傳遞給構造函數,用於區分不同線程池的線程
}
}
7、RejectedExecutionHandler
:當隊列和線程池都滿了的時候,根據拒絕策略處理新任務。
AbortPolicy:預設的策略,直接拋出RejectedExecutionException
DiscardPolicy:不處理,直接丟棄
DiscardOldestPolicy:將等待隊列隊首的任務丟棄,並執行當前任務
CallerRunsPolicy:由調用線程處理該任務
線程池大小怎麼設置?
如果線程池線程數量太小,當有大量請求需要處理,系統響應比較慢,會影響用戶體驗,甚至會出現任務隊列大量堆積任務導致OOM。
如果線程池線程數量過大,大量線程可能會同時搶占 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執行時間,影響了執行效率。
CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1
,多出來的一個線程是為了防止某些原因導致的線程阻塞(如IO操作,線程sleep,等待鎖)而帶來的影響。一旦某個線程被阻塞,釋放了CPU資源,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。
I/O 密集型任務(2N): 系統的大部分時間都在處理 IO 操作,此時線程可能會被阻塞,釋放CPU資源,這時就可以將 CPU 交出給其它線程使用。因此在 IO 密集型任務的應用中,可以多配置一些線程,具體的計算方法:最佳線程數 = CPU核心數 * (1/CPU利用率) = CPU核心數 * (1 + (IO耗時/CPU耗時))
,一般可設置為2N。
線程池的類型有哪些?適用場景?
常見的線程池有 FixedThreadPool
、SingleThreadExecutor
、CachedThreadPool
和 ScheduledThreadPool
。這幾個都是 ExecutorService
線程池實例。
FixedThreadPool
固定線程數的線程池。任何時間點,最多只有 nThreads 個線程處於活動狀態執行任務。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
使用無界隊列 LinkedBlockingQueue(隊列容量為 Integer.MAX_VALUE),運行中的線程池不會拒絕任務,即不會調用RejectedExecutionHandler.rejectedExecution()方法。
maxThreadPoolSize 是無效參數,故將它的值設置為與 coreThreadPoolSize 一致。
keepAliveTime 也是無效參數,設置為0L,因為此線程池裡所有線程都是核心線程,核心線程不會被回收(除非設置了executor.allowCoreThreadTimeOut(true))。
適用場景:適用於處理CPU密集型的任務,確保CPU在長期被工作線程使用的情況下,儘可能的少的分配線程,即適用執行長期的任務。需要註意的是,FixedThreadPool 不會拒絕任務,在任務比較多的時候會導致 OOM。
SingleThreadExecutor
只有一個線程的線程池。
public static ExecutionService newSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
使用無界隊列 LinkedBlockingQueue。線程池只有一個運行的線程,新來的任務放入工作隊列,線程處理完任務就迴圈從隊列里獲取任務執行。保證順序的執行各個任務。
適用場景:適用於串列執行任務的場景,一個任務一個任務地執行。在任務比較多的時候也是會導致 OOM。
CachedThreadPool
根據需要創建新線程的線程池。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
如果主線程提交任務的速度高於線程處理任務的速度時,CachedThreadPool
會不斷創建新的線程。極端情況下,這樣會導致耗盡 cpu 和記憶體資源。
使用沒有容量的SynchronousQueue作為線程池工作隊列,當線程池有空閑線程時,SynchronousQueue.offer(Runnable task)
提交的任務會被空閑線程處理,否則會創建新的線程處理任務。
適用場景:用於併發執行大量短期的小任務。CachedThreadPool
允許創建的線程數量為 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。
ScheduledThreadPoolExecutor
在給定的延遲後運行任務,或者定期執行任務。在實際項目中基本不會被用到,因為有其他方案選擇比如quartz
。
使用的任務隊列 DelayQueue
封裝了一個 PriorityQueue
,PriorityQueue
會對隊列中的任務進行排序,時間早的任務先被執行(即ScheduledFutureTask
的 time
變數小的先執行),如果time相同則先提交的任務會被先執行(ScheduledFutureTask
的 squenceNumber
變數小的先執行)。
執行周期任務步驟:
- 線程從
DelayQueue
中獲取已到期的ScheduledFutureTask(DelayQueue.take())
。到期任務是指ScheduledFutureTask
的 time 大於等於當前系統的時間; - 執行這個
ScheduledFutureTask
; - 修改
ScheduledFutureTask
的 time 變數為下次將要被執行的時間; - 把這個修改 time 之後的
ScheduledFutureTask
放回DelayQueue
中(DelayQueue.add()
)。
適用場景:周期性執行任務的場景,需要限制線程數量的場景。
一個項目使用多個線程池還是一個線程池?
項目中如果有多個場景需要使用線程池,那麼最好的方式是:每一個業務場景使用獨立的線程池。不要讓所有的場景共用一個線程池。
1)獨立的線城池之間互相不影響彼此的任務作業,更有利於保證本任務的獨立性和完整性,更符合低耦合的設計思想
2)如果所有的場景共用一個線程池,可能會出現問題,比如有任務A、任務B、任務C 這三個任務場景共用一個線程池。當任務A請求量劇烈增加的時候就會導致任務B和任務C,沒有可用的線程,可能出現遲遲獲取不到資源的情況。比如任務A同時有3000個線程請求,此時就可能會導致 任務B和任務C分配不到資源或者分配到很少的線程資源。
註:
1.JDK自帶的類使用了很多的線程池;
2.很多開源框架使用了大量的線程池;
3.自己的應用也會創建多個線程池;
4.多少個線程池,每個線程池提供多少線程,必須經過詳細的測試
execute和submit的區別
execute只能提交Runnable類型的任務,無返回值。submit既可以提交Runnable類型的任務,也可以提交Callable類型的任務,會有一個類型為Future的返回值,但當任務類型為Runnable時,返回值為null。
execute在執行任務時,如果遇到異常會直接拋出,而submit不會直接拋出,只有在使用Future的get方法獲取返回值時,才會拋出異常
execute所屬頂層介面是Executor,submit所屬頂層介面是ExecutorService,實現類ThreadPoolExecutor重寫了execute方法,抽象類AbstractExecutorService重寫了submit方法。
進程線程
進程是指一個記憶體中運行的應用程式,每個進程都有自己獨立的一塊記憶體空間。
線程是比進程更小的執行單位,它是在一個進程中獨立的控制流,一個進程可以啟動多個線程,每條線程並行執行不同的任務。
線程的生命周期
初始(NEW):線程被構建,還沒有調用 start()。
運行(RUNNABLE):包括操作系統的就緒和運行兩種狀態。
阻塞(BLOCKED):一般是被動的,在搶占資源中得不到資源,被動的掛起在記憶體,等待資源釋放將其喚醒。線程被阻塞會釋放CPU,不釋放記憶體。
等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。
終止(TERMINATED):表示該線程已經執行完畢。
圖片來源:Java併發編程的藝術
講講線程中斷?
線程中斷即線程運行過程中被其他線程給打斷了,它與 stop 最大的區別是:stop 是由系統強制終止線程,而線程中斷則是給目標線程發送一個中斷信號,如果目標線程沒有接收線程中斷的信號並結束線程,線程則不會終止,具體是否退出或者執行其他邏輯取決於目標線程。
線程中斷三個重要的方法:
1、java.lang.Thread#interrupt
調用目標線程的interrupt()
方法,給目標線程發一個中斷信號,線程被打上中斷標記。
2、java.lang.Thread#isInterrupted()
判斷目標線程是否被中斷,不會清除中斷標記。
3、java.lang.Thread#interrupted
判斷目標線程是否被中斷,會清除中斷標記。
private static void test2() {
Thread thread = new Thread(() -> {
while (true) {
Thread.yield();
// 響應中斷
if (Thread.currentThread().isInterrupted()) {
System.out.println("Java技術棧線程被中斷,程式退出。");
return;
}
}
});
thread.start();
thread.interrupt();
}
創建線程有哪幾種方式?
- 通過擴展
Thread
類來創建多線程 - 通過實現
Runnable
介面來創建多線程 - 實現
Callable
介面,通過FutureTask
介面創建線程。 - 使用
Executor
框架來創建線程池。
繼承 Thread 創建線程代碼如下。run()方法是由jvm創建完操作系統級線程後回調的方法,不可以手動調用,手動調用相當於調用普通方法。
/**
* @author: 程式員大彬
* @time: 2021-09-11 10:15
*/
public class MyThread extends Thread {
public MyThread() {
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
public static void main(String[] args) {
MyThread mThread1 = new MyThread();
MyThread mThread2 = new MyThread();
MyThread myThread3 = new MyThread();
mThread1.start();
mThread2.start();
myThread3.start();
}
}
Runnable 創建線程代碼:
/**
* @author: 程式員大彬
* @time: 2021-09-11 10:04
*/
public class RunnableTest {
public static void main(String[] args){
Runnable1 r = new Runnable1();
Thread thread = new Thread(r);
thread.start();
System.out.println("主線程:["+Thread.currentThread().getName()+"]");
}
}
class Runnable1 implements Runnable{
@Override
public void run() {
System.out.println("當前線程:"+Thread.currentThread().getName());
}
}
實現Runnable介面比繼承Thread類所具有的優勢:
- 可以避免java中的單繼承的限制
- 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類
Callable 創建線程代碼:
/**
* @author: 程式員大彬
* @time: 2021-09-11 10:21
*/
public class CallableTest {
public static void main(String[] args) {
Callable1 c = new Callable1();
//非同步計算的結果
FutureTask<Integer> result = new FutureTask<>(c);
new Thread(result).start();
try {
//等待任務完成,返回結果
int sum = result.get();
System.out.println(sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class Callable1 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
使用 Executor 創建線程代碼:
/**
* @author: 程式員大彬
* @time: 2021-09-11 10:44
*/
public class ExecutorsTest {
public static void main(String[] args) {
//獲取ExecutorService實例,生產禁用,需要手動創建線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//提交任務
executorService.submit(new RunnableDemo());
}
}
class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("大彬");
}
}
什麼是線程死鎖?
線程死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的一種互相等待的現象。若無外力作用,它們都將無法推進下去。
如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方持有的資源,所以這兩個線程就會互相等待而進入死鎖狀態。
下麵通過例子說明線程死鎖,代碼來自併發編程之美。
public class DeadLockDemo {
private static Object resource1 = new Object();//資源 1
private static Object resource2 = new Object();//資源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "線程 2").start();
}
}
代碼輸出如下:
Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1
線程 A 通過 synchronized
(resource1) 獲得 resource1 的監視器鎖,然後通過 Thread.sleep(1000)
。讓線程 A 休眠 1s 為的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。
線程死鎖怎麼產生?怎麼避免?
死鎖產生的四個必要條件:
-
互斥:一個資源每次只能被一個進程使用
-
請求與保持:一個進程因請求資源而阻塞時,不釋放獲得的資源
-
不剝奪:進程已獲得的資源,在未使用之前,不能強行剝奪
-
迴圈等待:進程之間迴圈等待著資源
避免死鎖的方法:
- 互斥條件不能破壞,因為加鎖就是為了保證互斥
- 一次性申請所有的資源,避免線程占有資源而且在等待其他資源
- 占有部分資源的線程進一步申請其他資源時,如果申請不到,主動釋放它占有的資源
- 按序申請資源
線程run和start的區別?
- 當程式調用
start()
方法,將會創建一個新線程去執行run()
方法中的代碼。run()
就像一個普通方法一樣,直接調用run()
的話,不會創建新線程。 - 一個線程的
start()
方法只能調用一次,多次調用會拋出 java.lang.IllegalThreadStateException 異常。run()
方法則沒有限制。
線程都有哪些方法?
start
用於啟動線程。
getPriority
獲取線程優先順序,預設是5,線程預設優先順序為5,如果不手動指定,那麼線程優先順序具有繼承性,比如線程A啟動線程B,那麼線程B的優先順序和線程A的優先順序相同
setPriority
設置線程優先順序。CPU會儘量將執行資源讓給優先順序比較高的線程。
interrupt
告訴線程,你應該中斷了,具體到底中斷還是繼續運行,由被通知的線程自己處理。
當對一個線程調用 interrupt() 時,有兩種情況:
-
如果線程處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼線程將立即退出被阻塞狀態,並拋出一個InterruptedException異常。
-
如果線程處於正常活動狀態,那麼會將該線程的中斷標誌設置為 true。不過,被設置中斷標誌的線程可以繼續正常運行,不受影響。
interrupt() 並不能真正的中斷線程,需要被調用的線程自己進行配合才行。
join
等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉為就緒狀態。
yield
暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先順序的線程。
sleep
使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒為單位。當睡眠結束後,線程自動轉為Runnable狀態。
volatile底層原理
volatile
是輕量級的同步機制,volatile
保證變數對所有線程的可見性,不保證原子性。
- 當對
volatile
變數進行寫操作的時候,JVM會向處理器發送一條LOCK
首碼的指令,將該變數所在緩存行的數據寫回系統記憶體。 - 由於緩存一致性協議,每個處理器通過嗅探在匯流排上傳播的數據來檢查自己的緩存是不是過期了,當處理器發現自己緩存行對應的記憶體地址被修改,就會將當前處理器的緩存行置為無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統記憶體中把數據讀到處理器緩存中。
來看看緩存一致性協議是什麼。
緩存一致性協議:當CPU寫數據時,如果發現操作的變數是共用變數,即在其他CPU中也存在該變數的副本,會發出信號通知其他CPU將該變數的緩存行置為無效狀態,因此當其他CPU需要讀取這個變數時,就會從記憶體重新讀取。
volatile
關鍵字的兩個作用:
- 保證了不同線程對共用變數進行操作時的可見性,即一個線程修改了某個變數的值,這新值對其他線程來說是立即可見的。
- 禁止進行指令重排序。
指令重排序是JVM為了優化指令,提高程式運行效率,在不影響單線程程式執行結果的前提下,儘可能地提高並行度。Java編譯器會在生成指令系列時在適當的位置會插入
記憶體屏障
指令來禁止處理器重排序。插入一個記憶體屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。對一個volatile欄位進行寫操作,Java記憶體模型將在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都刷新到記憶體。
synchronized的用法有哪些?
- 修飾普通方法:作用於當前對象實例,進入同步代碼前要獲得當前對象實例的鎖
- 修飾靜態方法:作用於當前類,進入同步代碼前要獲得當前類對象的鎖,synchronized關鍵字加到static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖
- 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖
synchronized的作用有哪些?
原子性:確保線程互斥的訪問同步代碼;
可見性:保證共用變數的修改能夠及時可見;
有序性:有效解決重排序問題。
synchronized 底層實現原理?
synchronized 同步代碼塊的實現是通過 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代碼塊的開始位置,monitorexit
指令則指明同步代碼塊的結束位置。當執行 monitorenter
指令時,線程試圖獲取鎖也就是獲取 monitor
的持有權(monitor對象存在於每個Java對象的對象頭中, synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意對象可以作為鎖的原因)。
其內部包含一個計數器,當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在執行 monitorexit
指令後,將鎖計數器設為0
,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止
synchronized 修飾的方法並沒有 monitorenter
指令和 monitorexit
指令,取得代之的確實是ACC_SYNCHRONIZED
標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED
訪問標誌來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。
volatile和synchronized的區別是什麼?
volatile
只能使用在變數上;而synchronized
可以在類,變數,方法和代碼塊上。volatile
至保證可見性;synchronized
保證原子性與可見性。volatile
禁用指令重排序;synchronized
不會。volatile
不會造成阻塞;synchronized
會。
ReentrantLock和synchronized區別
- 使用synchronized關鍵字實現同步,線程執行完同步代碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
- synchronized是非公平鎖,ReentrantLock可以設置為公平鎖。
- ReentrantLock上等待獲取鎖的線程是可中斷的,線程可以放棄等待鎖。而synchonized會無限期等待下去。
- ReentrantLock 可以設置超時獲取鎖。在指定的截止時間之前獲取鎖,如果截止時間到了還沒有獲取到鎖,則返回。
- ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的獲取鎖,調用該方法後立刻返回,如果能夠獲取則返回true,否則返回false。
wait()和sleep()的異同點?
相同點:
- 它們都可以使當前線程暫停運行,把機會交給其他線程
- 任何線程在調用wait()和sleep()之後,在等待期間被中斷都會拋出
InterruptedException
不同點:
wait()
是Object超類中的方法;而sleep()
是線程Thread類中的方法- 對鎖的持有不同,
wait()
會釋放鎖,而sleep()
並不釋放鎖 - 喚醒方法不完全相同,
wait()
依靠notify
或者notifyAll
、中斷、達到指定時間來喚醒;而sleep()
到達指定時間被喚醒 - 調用
wait()
需要先獲取對象的鎖,而Thread.sleep()
不用
Runnable和Callable有什麼區別?
- Callable介面方法是
call()
,Runnable的方法是run()
; - Callable介面call方法有返回值,支持泛型,Runnable介面run方法無返回值。
- Callable介面
call()
方法允許拋出異常;而Runnable介面run()
方法不能繼續上拋異常。
線程執行順序怎麼控制?
假設有T1、T2、T3三個線程,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?
可以使用join方法解決這個問題。比如線上程A中,調用線程B的join方法表示的意思就是:A等待B線程執行完畢後(釋放CPU執行權),在繼續執行。
代碼如下:
public class ThreadTest {
public static void main(String[] args) {
Thread spring = new Thread(new SeasonThreadTask("春天"));
Thread summer = new Thread(new SeasonThreadTask("夏天"));
Thread autumn = new Thread(new SeasonThreadTask("秋天"));
try
{
//春天線程先啟動
spring.start();
//主線程等待線程spring執行完,再往下執行
spring.join();
//夏天線程再啟動
summer.start();
//主線程等待線程summer執行完,再往下執行
summer.join();
//秋天線程最後啟動
autumn.start();
//主線程等待線程autumn執行完,再往下執行
autumn.join();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
class SeasonThreadTask implements Runnable{
private String name;
public SeasonThreadTask(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <4; i++) {
System.out.println(this.name + "來了: " + i + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結果:
春天來了: 1次
春天來了: 2次
春天來了: 3次
夏天來了: 1次
夏天來了: 2次
夏天來了: 3次
秋天來了: 1次
秋天來了: 2次
秋天來了: 3次
守護線程是什麼?
守護線程是運行在後臺的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。
線程間通信方式
1、使用 Object 類的 wait()/notify()。Object 類提供了線程間通信的方法:wait()
、notify()
、notifyAll()
,它們是多線程通信的基礎。其中,wait/notify
必須配合 synchronized
使用,wait 方法釋放鎖,notify 方法不釋放鎖。wait 是指在一個已經進入了同步鎖的線程內,讓自己暫時讓出同步鎖,以便其他正在等待此鎖的線程可以得到同步鎖並運行,只有其他線程調用了notify()
,notify並不釋放鎖,只是告訴調用過wait()
的線程可以去參與獲得鎖的競爭了,但不是馬上得到鎖,因為鎖還在別人手裡,別人還沒釋放,調用 wait()
的一個或多個線程就會解除 wait 狀態,重新參與競爭對象鎖,程式如果可以再次得到鎖,就可以繼續向下運行。
2、使用 volatile 關鍵字。基於volatile關鍵字實現線程間相互通信,其底層使用了共用記憶體。簡單來說,就是多個線程同時監聽一個變數,當這個變數發生變化的時候 ,線程能夠感知並執行相應的業務。
3、使用JUC工具類 CountDownLatch。jdk1.5 之後在java.util.concurrent
包下提供了很多併發編程相關的工具類,簡化了併發編程開發,CountDownLatch
基於 AQS 框架,相當於也是維護了一個線程間共用變數 state。
4、基於 LockSupport 實現線程間的阻塞和喚醒。LockSupport
是一種非常靈活的實現線程間阻塞和喚醒的工具,使用它不用關註是等待線程先進行還是喚醒線程先運行,但是得知道線程的名字。
ThreadLocal
線程本地變數。當使用ThreadLocal
維護變數時,ThreadLocal
為每個使用該變數的線程提供獨立的變數副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程。
ThreadLocal原理
每個線程都有一個ThreadLocalMap
(ThreadLocal
內部類),Map中元素的鍵為ThreadLocal
,而值對應線程的變數副本。
調用threadLocal.set()
-->調用getMap(Thread)
-->返回當前線程的ThreadLocalMap<ThreadLocal, value>
-->map.set(this, value)
,this是threadLocal
本身。源碼如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
調用get()
-->調用getMap(Thread)
-->返回當前線程的ThreadLocalMap<ThreadLocal, value>
-->map.getEntry(this)
,返回value
。源碼如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
threadLocals
的類型ThreadLocalMap
的鍵為ThreadLocal
對象,因為每個線程中可有多個threadLocal
變數,如longLocal
和stringLocal
。
public class ThreadLocalDemo {
ThreadLocal<Long> longLocal = new ThreadLocal<>();
public void set() {
longLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longLocal.get();
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
Thread thread = new Thread(() -> {
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
}
);
thread.start();
thread.join();
System.out.println(threadLocalDemo.get());
}
}
ThreadLocal
並不是用來解決共用資源的多線程訪問問題,因為每個線程中的資源只是副本,不會共用。因此ThreadLocal
適合作為線程上下文變數,簡化線程內傳參。
ThreadLocal記憶體泄漏的原因?
每個線程都有⼀個ThreadLocalMap
的內部屬性,map的key是ThreaLocal
,定義為弱引用,value是強引用類型。垃圾回收的時候會⾃動回收key,而value的回收取決於Thread對象的生命周期。一般會通過線程池的方式復用線程節省資源,這也就導致了線程對象的生命周期比較長,這樣便一直存在一條強引用鏈的關係:Thread
--> ThreadLocalMap
-->Entry
-->Value
,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致記憶體泄漏。
解決⽅法:每次使⽤完ThreadLocal
就調⽤它的remove()
⽅法,手動將對應的鍵值對刪除,從⽽避免記憶體泄漏。
ThreadLocal使用場景有哪些?
場景1
ThreadLocal 用作保存每個線程獨享的對象,為每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
這種場景通常用於保存線程不安全的工具類,典型的使用的類就是 SimpleDateFormat。
假如需求為500個線程都要用到 SimpleDateFormat,使用線程池來實現線程的復用,否則會消耗過多的記憶體等資源,如果我們每個任務都創建了一個 simpleDateFormat 對象,也就是說,500個任務對應500個 simpleDateFormat 對象。但是這麼多對象的創建是有開銷的,而且這麼多對象同時存在在記憶體中也是一種記憶體的浪費。可以將simpleDateFormat 對象給提取了出來,變成靜態變數,但是這樣一來就會有線程不安全的問題。我們想要的效果是,既不浪費過多的記憶體,同時又想保證線程安全。此時,可以使用 ThreadLocal來達到這個目的,每個線程都擁有一個自己的 simpleDateFormat 對象。
場景2
ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息後,後續方法可以通過 ThreadLocal 直接獲取到,避免了傳參,類似於全局變數的概念。
比如Java web應用中,每個線程有自己單獨的Session
實例,就可以使用ThreadLocal
來實現。
AQS原理
AQS,AbstractQueuedSynchronizer
,抽象隊列同步器,定義了一套多線程訪問共用資源的同步器框架,許多併發工具的實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch
。
AQS使用一個volatile
的int類型的成員變數state
來表示同步狀態,通過CAS修改同步狀態的值。當線程調用 lock 方法時 ,如果 state
=0,說明沒有任何線程占有共用資源的鎖,可以獲得鎖並將 state
加1。如果 state
不為0,則說明有線程目前正在使用共用變數,其他線程必須加入同步隊列進行等待。
private volatile int state;//共用變數,使用volatile修飾保證線程可見性
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態(獨占或共用 )構造成為一個節點(Node)並將其加入同步隊列併進行自旋,當同步狀態釋放時,會把首節點中的後繼節點對應的線程喚醒,使其再次嘗試獲取同步狀態。
ReentrantLock 是如何實現可重入性的?
ReentrantLock
內部自定義了同步器sync,在加鎖的時候通過CAS演算法,將線程對象放到一個雙向鏈表中,每次獲取鎖的時候,檢查當前維護的那個線程ID和當前請求的線程ID是否 一致,如果一致,同步狀態加1,表示鎖被當前線程獲取了多次。
源碼如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
鎖的分類
公平鎖與非公平鎖
按照線程訪問順序獲取對象鎖。synchronized
是非公平鎖,Lock
預設是非公平鎖,可以設置為公平鎖,公平鎖會影響性能。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
共用式與獨占式鎖
共用式與獨占式的最主要區別在於:同一時刻獨占式只能有一個線程獲取同步狀態,而共用式在同一時刻可以有多個線程獲取同步狀態。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作,其他操作都會被阻塞。
悲觀鎖與樂觀鎖
悲觀鎖,每次訪問資源都會加鎖,執行完同步代碼釋放鎖,synchronized
和ReentrantLock
屬於悲觀鎖。
樂觀鎖,不會鎖定資源,所有的線程都能訪問並修改同一個資源,如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。樂觀鎖最常見的實現就是CAS
。
適用場景:
- 悲觀鎖適合寫操作多的場景。
- 樂觀鎖適合讀操作多的場景,不加鎖可以提升讀操作的性能。
樂觀鎖有什麼問題?
樂觀鎖避免了悲觀鎖獨占對象的問題,提高了併發性能,但它也有缺點:
- 樂觀鎖只能保證一個共用變數的原子操作。
- 長時間自旋可能導致開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷。
- ABA問題。CAS的原理是通過比對記憶體值與預期值是否一樣而判斷記憶體值是否被改過,但是會有以下問題:假如記憶體值原來是A, 後來被一條線程改為B,最後又被改成了A,則CAS認為此記憶體值並沒有發生改變。可以引入版本號解決這個問題,每次變數更新都把版本號加一。
什麼是CAS?
CAS全稱Compare And Swap
,比較與交換,是樂觀鎖的主要實現方式。CAS在不使用鎖的情況下實現多線程之間的變數同步。ReentrantLock
內部的AQS和原子類內部都使用了CAS。
CAS演算法涉及到三個操作數:
- 需要讀寫的記憶體值V。
- 進行比較的值A。
- 要寫入的新值B。
只有當V的值等於A時,才會使用原子方式用新值B來更新V的值,否則會繼續重試直到成功更新值。
以AtomicInteger
為例,AtomicInteger
的getAndIncrement()
方法底層就是CAS實現,關鍵代碼是 compareAndSwapInt(obj, offset, expect, update)
,其含義就是,如果obj
內的value
和expect
相等,就證明沒有其他線程改變過這個變數,那麼就更新它為update
,如果不相等,那就會繼續重試直到成功更新值。
CAS存在的問題?
CAS 三大問題:
-
ABA問題。CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面添加版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從
A-B-A
變成了1A-2B-3A
。JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,原子更新帶有版本號的引用類型。
-
迴圈時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
-
只能保證一個共用變數的原子操作。對一個共用變數執行操作時,CAS能夠保證原子操作,但是對多個共用變數操作時,CAS是無法保證操作的原子性的。
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變數放在一個對象里來進行CAS操作。
原子類
基本類型原子類
使用原子的方式更新基本類型
- AtomicInteger:整型原子類
- AtomicLong:長整型原子類
- AtomicBoolean :布爾型原子類
AtomicInteger 類常用的方法:
public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設置為輸入值(update)
public final void lazySet(int newValue)//最終設置為newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。
AtomicInteger 類主要利用 CAS (compare and swap) 保證原子操作,從而避免加鎖的高開銷。
數組類型原子類
使用原子的方式更新數組裡的某個元素
- AtomicIntegerArray:整形數組原子類
- AtomicLongArray:長整形數組原子類
- AtomicReferenceArray :引用類型數組原子類
AtomicIntegerArray 類常用方法:
public final int get(int i) //獲取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的當前的值,並將其設置為新值:newValue
public final int getAndIncrement(int i)//獲取 index=i 位置元素的值,並讓該位置的元素自增
public final int getAndDecrement(int i) //獲取 index=i 位置元素的值,並讓該位置的元素自減
public final int getAndAdd(int i, int delta) //獲取 index=i 位置元素的值,並加上預期的值
boolean compareAndSet(int i, int expect, int update) //如果輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設置為輸入值(update)
public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設置為newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。
引用類型原子類
- AtomicReference:引用類型原子類
- AtomicStampedReference:帶有版本號的引用類型原子類。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
- AtomicMarkableReference :原子更新帶有標記的引用類型。該類將 boolean 標記與引用關聯起來
為什麼要使用Executor線程池框架呢?
- 每次執行任務都通過new Thread()去創建線程,比較消耗性能,創建一個線程是比較耗時、耗資源的
- 調用new Thread()創建的線程缺乏管理,可以無限制的創建,線程之間的相互競爭會導致過多占用系統資源而導致系統癱瘓
- 直接使用new Thread()啟動的線程不利於擴展,比如定時執行、定期執行、定時定期執行、線程中斷等都不好實現
如何停止一個正在運行的線程?
- 使用共用變數的方式。共用變數可以被多個執行相同任務的線程用來作為是否停止的信號,通知停止線程的執行。
- 使用interrupt方法終止線程。當一個線程被阻塞,處於不可運行狀態時,即使主程式中將該線程的共用變數設置為true,但該線程此時根本無法檢查迴圈標誌,當然也就無法立即中斷。這時候可以使用Thread提供的interrupt()方法,因為該方法雖然不會中斷一個正在運行的線程,但是它可以使一個被阻塞的線程拋出一個中斷異常,從而使線程提前結束阻塞狀態。
什麼是Daemon線程?
後臺(daemon)線程,是指在程式運行的時候在後臺提供一種通用服務的線程,並且這個線程並不屬於程式中不可或缺的部分。因此,當所有的非後臺線程結束時,程式也就終止了,同時會殺死進程中的所有後臺線程。反過來說,只要有任何非後臺線程還在運行,程式就不會終止。必須線上程啟動之前調用setDaemon()方法,才能把它設置為後臺線程。
註意:後臺進程在不執行finally子句的情況下就會終止其run()方法。
比如:JVM的垃圾回收線程就是Daemon線程,Finalizer也是守護線程。
SynchronizedMap和ConcurrentHashMap有什麼區別?
SynchronizedMap一次鎖住整張表來保證線程安全,所以每次只能有一個線程來訪問map。
JDK1.8 ConcurrentHashMap採用CAS和synchronized來保證併發安全。數據結構採用數組+鏈表/紅黑二叉樹。synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,支持併發訪問、修改。
另外ConcurrentHashMap使用了一種不同的迭代方式。當iterator被創建後集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據 ,iterator完成後再將頭指針替換為新的數據 ,這樣iterator線程可以使用原來老的數據,而寫線程也可以併發的完成改變。
怎麼判斷線程池的任務是不是執行完了?
有幾種方法:
1、使用線程池的原生函數isTerminated();
executor提供一個原生函數isTerminated()來判斷線程池中的任務是否全部完成。如果全部完成返回true,否則返回false。
2、使用重入鎖,維持一個公共計數。
所有的普通任務維持一個計數器,當任務完成時計數器加一(這裡要加鎖),當計數器的值等於任務數時,這時所有的任務已經執行完畢了。
3、使用CountDownLatch。
它的原理跟第二種方法類似,給CountDownLatch一個計數值,任務執行完畢後,調用countDown()執行計數值減一。最後執行的任務在調用方法的開始調用await()方法,這樣整個任務會阻塞,直到這個計數值為零,才會繼續執行。
這種方式的缺點就是需要提前知道任務的數量。
4、submit向線程池提交任務,使用Future判斷任務執行狀態。
使用submit向線程池提交任務與execute提交不同,submit會有Future類型的返回值。通過future.isDone()方法可以知道任務是否執行完成。
什麼是Future?
在併發編程中,不管是繼承thread類還是實現runnable介面,都無法保證獲取到之前的執行結果。通過實現Callback介面,並用Future可以來接收多線程的執行結果。
Future表示一個可能還沒有完成的非同步任務的結果,針對這個結果可以添加Callback以便在任務執行成功或失敗後作出相應的操作。
舉個例子:比如去吃早點時,點了包子和冷盤,包子需要等3分鐘,冷盤只需1分鐘,如果是串列的一個執行,在吃上早點的時候需要等待4分鐘,但是因為你在等包子的時候,可以同時準備冷盤,所以在準備冷盤的過程中,可以同時準備包子,這樣只需要等待3分鐘。Future就是後面這種執行模式。
Future介面主要包括5個方法:
- get()方法可以當任務結束後返回一個結果,如果調用時,工作還沒有結束,則會阻塞線程,直到任務執行完畢
- get(long timeout,TimeUnit unit)做多等待timeout的時間就會返回結果
- cancel(boolean mayInterruptIfRunning)方法可以用來停止一個任務,如果任務可以停止(通過mayInterruptIfRunning來進行判斷),則可以返回true,如果任務已經完成或者已經停止,或者這個任務無法停止,則會返回false。
- isDone()方法判斷當前方法是否完成
- isCancel()方法判斷當前方法是否取消
最後給大家分享200多本電腦經典書籍PDF電子書,包括C語言、C++、Java、Python、前端、資料庫、操作系統、電腦網路、數據結構和演算法、機器學習、編程人生等,感興趣的小伙伴可以自取: