Java並發編程八股文!

程序員大彬 2021-09-19 12:22:47 阅读数:586

java 八股文 八股

Java並發

大家好,我是大彬。最近在面試,看了很多面經,將常見的Java並發編程常見面試題總結了一下,如果對你有幫助,可以 收藏和點贊後續還會繼續更新新的面試題目哦!

文章目錄如下:

首先給大家分享一個github倉庫,上面放了200多本經典的計算機書籍,包括C語言、C++、Java、Python、前端、數據庫、操作系統、計算機網絡、數據結構和算法、機器學習、編程人生等,可以star一下,下次找書直接在上面搜索,倉庫持續更新中~

github地址:https://github.com/Tyson0314/...

如果github訪問不了,可以訪問gitee倉庫。

gitee地址:https://gitee.com/tysondai/ja...

線程池

線程池:一個管理線程的池子。

為什麼使用線程池?

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷毀造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。統一管理線程,避免系統創建大量同類線程而導致消耗完內存。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

線程池執行原理?

創建新的線程需要獲取全局鎖,通過這種設計可以盡量避免獲取全局鎖,當 ThreadPoolExecutor 完成預熱之後(當前運行的線程數大於等於 corePoolSize),提交的大部分任務都會被放到 BlockingQueue。

為了形象描述線程池執行,打個比喻:

  • 核心線程比作公司正式員工
  • 非核心線程比作外包員工
  • 阻塞隊列比作需求池
  • 提交任務比作提需求

線程池參數有哪些?

ThreadPoolExecutor 的通用構造函數:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:當有新任務時,如果線程池中線程數沒有達到線程池的基本大小,則會創建新的線程執行任務,否則將任務放入阻塞隊列。當線程池中存活的線程數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
  • maximumPoolSize:當阻塞隊列填滿時,如果線程池中線程數沒有超過最大線程數,則會創建新的線程運行任務。否則根據拒絕策略處理新任務。非核心線程類似於臨時借來的資源,這些線程在空閑時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
  • BlockingQueue:存儲等待運行的任務。
  • keepAliveTime:非核心線程空閑後,保持存活的時間,此參數只對非核心線程有效。設置為0,錶示多餘的空閑線程會被立即終止。
  • TimeUnit:時間單比特

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
  • 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);//將線程池名字傳遞給構造函數,用於區分不同線程池的線程
    }
    }
  • RejectedExecutionHandler:當隊列和線程池都滿了時,根據拒絕策略處理新任務。

    AbortPolicy:默認的策略,直接拋出RejectedExecutionException
    DiscardPolicy:不處理,直接丟弃
    DiscardOldestPolicy:將等待隊列隊首的任務丟弃,並執行當前任務
    CallerRunsPolicy:由調用線程處理該任務

線程池大小怎麼設置?

如果線程池線程數量太小,當有大量請求需要處理,系統響應比較慢影響體驗,甚至會出現任務隊列大量堆積任務導致OOM。

如果線程池線程數量過大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換(cpu給線程分配時間片,當線程的cpu時間片用完後保存狀態,以便下次繼續運行),從 而增加線程的執行時間,影響了整體執行效率。

CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是為了防止某些原因導致的任務暫停(線程阻塞,如io操作,等待鎖,線程sleep)而帶來的影響。一旦某個線程被阻塞,釋放了cpu資源,而在這種情况下多出來的一個線程就可以充分利用 CPU 的空閑時間。

I/O 密集型任務(2N): 系統會用大部分的時間來處理 I/O 操作,而線程等待 I/O 操作會被阻塞,釋放 cpu資源,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法:最佳線程數 = CPU核心數 (1/CPU利用率) = CPU核心數 (1 + (I/O耗時/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 封裝了一個 PriorityQueuePriorityQueue 會對隊列中的任務進行排序,時間早的任務先被執行(即ScheduledFutureTasktime 變量小的先執行),如果time相同則先提交的任務會被先執行(ScheduledFutureTasksquenceNumber 變量小的先執行)。

執行周期任務步驟:

  1. 線程從 DelayQueue 中獲取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任務是指 ScheduledFutureTask的 time 大於等於當前系統的時間;
  2. 執行這個 ScheduledFutureTask
  3. 修改 ScheduledFutureTask 的 time 變量為下次將要被執行的時間;
  4. 把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

適用場景:周期性執行任務的場景,需要限制線程數量的場景。

進程線程

進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,一個進程中可以啟動多個線程。
線程是比進程更小的執行單比特,它是在一個進程中獨立的控制流,一個進程可以啟動多個線程,每條線程並行執行不同的任務。

線程的生命周期

初始(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類所具有的優勢:

  1. 資源共享,適合多個相同的程序代碼的線程去處理同一個資源
  2. 可以避免java中的單繼承的限制
  3. 線程池只能放入實現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() 方法,無法達到啟動多線程的目的,相當於主線程線性執行 Thread 對象的 run() 方法。
一個線程對線的 start() 方法只能調用一次,多次調用會拋出 java.lang.IllegalThreadStateException 异常;run() 方法沒有限制。

線程都有哪些方法?

join

Thread.join(),在main中創建了thread線程,在main中調用了thread.join()/thread.join(long millis),main線程放弃cpu控制權,線程進入WAITING/TIMED_WAITING狀態,等到thread線程執行完才繼續執行main線程。

public final void join() throws InterruptedException {
join(0);
}

yield

Thread.yield(),一定是當前線程調用此方法,當前線程放弃獲取的CPU時間片,但不釋放鎖資源,由運行狀態變為就緒狀態,讓OS再次選擇線程。作用:讓相同優先級的線程輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由用戶指定暫停多長時間。

public static native void yield(); //static方法

sleep

Thread.sleep(long millis),一定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,讓出cpu資源,但不釋放對象鎖,指定時間到後又恢複運行。作用:給其它線程執行機會的最佳方式。

public static native void sleep(long millis) throws InterruptedException;//static方法

volatile底層原理

volatile是輕量級的同步機制,volatile保證變量對所有線程的可見性,不保證原子性。

  1. 當對volatile變量進行寫操作的時候,JVM會向處理器發送一條LOCK前綴的指令,將該變量所在緩存行的數據寫回系統內存。
  2. 由於緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己的緩存是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行置為無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存中。

MESI(緩存一致性協議):當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,就會從內存重新讀取。

volatile關鍵字的兩個作用:

  1. 保證了不同線程對共享變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
  2. 禁止進行指令重排序。

指令重排序是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高並行度。Java編譯器會在生成指令系列時在適當的比特置會插入內存屏障指令來禁止處理器重排序。插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。對一個volatile字段進行寫操作,Java內存模型將在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都刷新到內存。

AQS原理

AQS,AbstractQueuedSynchronizer,抽象隊列同步器,定義了一套多線程訪問共享資源的同步器框架,許多並發工具的實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一個volatile的int類型的成員變量state來錶示同步狀態,通過CAS修改同步狀態的值。當線程調用 lock 方法時 ,如果 state=0,說明沒有任何線程占有共享資源的鎖,可以獲得鎖並將 state=1。如果 state=1,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待。

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

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態(獨占或共享 )構造成為一個節點(Node)並將其加入同步隊列並進行自旋,當同步狀態釋放時,會把首節中的後繼節點對應的線程喚醒,使其再次嘗試獲取同步狀態。

synchronized的用法有哪些?

  1. 修飾普通方法:作用於當前對象實例,進入同步代碼前要獲得當前對象實例的鎖
  2. 修飾靜態方法:作用於當前類,進入同步代碼前要獲得當前類對象的鎖,synchronized關鍵字加到static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖
  3. 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖

Synchronized的作用有哪些?

原子性:確保線程互斥的訪問同步代碼;
可見性:保證共享變量的修改能够及時可見,其實是通過Java內存模型中的 “對一個變量unlock 操作之前,必須要同步到主內存中;如果對一個變量進行lock操作,則將會清空工作內存中此變量的值,在執行引擎使用此變量前,需要重新從主內存中load操作或assign操作初始化變量值” 來保證的;
有序性:有效解决重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”。

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 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。

ReentrantLock 是如何實現可重入性的?

ReentrantLock 內部自定義了同步器 Sync,在加鎖的時候通過 CAS 算法,將線程對象放到一個雙向鏈錶中,每次獲取鎖的時候,檢查當前維護的那個線程 ID 和當前請求的線程 ID 是否 一致,如果一致,同步狀態加1,錶示鎖被當前線程獲取了多次。

ReentrantLock和synchronized區別

  1. 使用synchronized關鍵字實現同步,線程執行完同步代碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
  2. synchronized是非公平鎖,ReentrantLock可以設置為公平鎖。
  3. ReentrantLock上等待獲取鎖的線程是可中斷的,線程可以放弃等待鎖。而synchonized會無限期等待下去。
  4. ReentrantLock 可以設置超時獲取鎖。在指定的截止時間之前獲取鎖,如果截止時間到了還沒有獲取到鎖,則返回。
  5. ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的獲取鎖,調用該方法後立刻返回,如果能够獲取則返回true,否則返回false。

wait()和sleep()的區別

相同點:

  1. 使當前線程暫停運行,把機會交給其他線程
  2. 任何線程在等待期間被中斷都會拋出InterruptedException

不同點:

  1. wait() 是Object超類中的方法;而sleep()是線程Thread類中的方法
  2. 對鎖的持有不同,wait()會釋放鎖,而sleep()並不釋放鎖
  3. 喚醒方法不完全相同,wait() 依靠notify或者notifyAll 、中斷、達到指定時間來喚醒;而sleep()到達指定時間被喚醒
  4. 調用obj.wait()需要先獲取對象的鎖,而 Thread.sleep()不用

wait(),notify()和suspend(),resume()之間的區別

  • wait() 使得線程進入阻塞等待狀態,並且釋放鎖
  • notify()喚醒一個處於等待狀態的線程,它一般跟wait()方法配套使用。
  • suspend()使得線程進入阻塞狀態,並且不會自動恢複,必須對應的resume() 被調用,才能使得線程重新進入可執行狀態。suspend()方法很容易引起死鎖問題。
  • resume()方法跟suspend()方法配套使用。

suspend()不建議使用,suspend()方法在調用後,線程不會釋放已經占有的資 源(比如鎖),而是占有著資源進入睡眠狀態,這樣容易引發死鎖問題。

Runnable和 Callable有什麼區別?

  • Callable接口方法是call(),Runnable的方法是run();
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法無返回值。
  • Callable接口call()方法允許拋出异常;而Runnable接口run()方法不能繼續上拋异常;

volatile和synchronized的區別是什麼?

  1. volatile只能使用在變量上;而synchronized可以在類,變量,方法和代碼塊上。
  2. volatile至保證可見性;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序;synchronized不會。
  4. volatile不會造成阻塞;synchronized會。

線程執行順序怎麼控制?

假設有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次

樂觀鎖一定就是好的嗎?

樂觀鎖避免了悲觀鎖獨占對象的現象,提高了並發性能,但它也有缺點:

  • 樂觀鎖只能保證一個共享變量的原子操作。如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解决,不管對象數量多少及對象顆粒度大小。
  • 長時間自旋可能導致開銷大。假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷。
  • ABA 問題。CAS 的核心思想是通過比對內存值與預期值是否一樣而判 斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是 A, 後來被一條線程改為 B,最後又被改成了 A,則 CAS 認為此內存值並 沒有發生改變,但實際上是有被其他線程改過的,這種情况對依賴過程值的情景的運算結果影響很大。解决的思路是引入版本號,每次變量更新都把版本號加一。

守護線程是什麼?

守護線程是運行在後臺的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些 發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。

線程間通信方式

volatile

volatile是輕量級的同步機制,volatile保證變量對所有線程的可見性,不保證原子性。

synchronized

保證線程對變量訪問的可見性和排他性。

等待通知機制

wait/notify為 Object 對象的方法,調用wait/notify需要先獲得對象的鎖。對象調用wait之後線程釋放鎖,將線程放到對象的等待隊列,當通知線程調用此對象的notify()方法後,等待線程並不會立即從wait返回,需要等待通知線程釋放鎖(通知線程執行完同步代碼塊),等待隊列裏的線程獲取鎖,獲取鎖成功才能從wait()方法返回,即從wait方法返回前提是線程獲得鎖。

等待通知機制依托於同步機制,目的是確保等待線程從wait方法返回時能感知到通知線程對對象的變量值的修改。

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內存泄漏的原因?

每個Thread都有⼀個ThreadLocalMap的內部屬性,map的key是ThreaLocal,定義為弱引用,value是强引用類型。GC的時候會⾃動回收key,而value的回收取决於Thread對象的生命周期。一般會通過線程池的方式複用Thread對象節省資源,這也就導致了Thread對象的生命周期比較長,這樣便一直存在一條强引用鏈的關系:Thread --> ThreadLocalMap-->Entry-->Value,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致內存泄漏。

image-20200715235804982

解决⽅法:每次使⽤完ThreadLocal就調⽤它的remove()⽅法,手動將對應的鍵值對删除,從⽽避免內存泄漏。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal使用場景有哪些?

ThreadLocal 適用場景:每個線程需要有自己單獨的實例,且需要在多個方法中共享實例,即同時滿足實例在線程間的隔離與方法間的共享。比如Java web應用中,每個線程有自己單獨的 Session 實例,就可以使用ThreadLocal來實現。

鎖的分類

公平鎖與非公平鎖

按照線程訪問順序獲取對象鎖。synchronized 是非公平鎖, Lock 默認是非公平鎖,可以設置為公平鎖,公平鎖會影響性能。

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

共享式與獨占式鎖

共享式與獨占式的最主要區別在於:同一時刻獨占式只能有一個線程獲取同步狀態,而共享式在同一時刻可以有多個線程獲取同步狀態。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作,其他操作都會被阻塞。

悲觀鎖與樂觀鎖

悲觀鎖,每次訪問資源都會加鎖,執行完同步代碼釋放鎖,synchronized 和 ReentrantLock 屬於悲觀鎖。

樂觀鎖,不會鎖定資源,所有的線程都能訪問並修改同一個資源,如果沒有沖突就修改成功並退出,否則就會繼續循環嘗試。樂觀鎖最常見的實現就是CAS。

樂觀鎖一般來說有以下2種方式:

  1. 使用數據版本記錄機制實現,這是樂觀鎖最常用的一種實現方式。給數據增加一個版本標識,一般是通過為數據庫錶增加一個數字類型的version字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫錶對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫錶當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。
  2. 使用時間戳。數據庫錶增加一個字段,字段類型使用時間戳(timestamp),和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。

適用場景:

  • 悲觀鎖適合寫操作多的場景。
  • 樂觀鎖適合讀操作多的場景,不加鎖可以提昇讀操作的性能。

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內的valueexpect相等,就證明沒有其他線程改變過這個變量,那麼就更新它為update,如果不相等,那就會繼續重試直到成功更新值。

CAS存在的問題?

CAS 三大問題:

  1. ABA問題。CAS需要在操作值的時候檢查內存值是否發生變化,沒有發生變化才會更新內存值。但是如果內存值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解决思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從A-B-A變成了1A-2B-3A

    JDK從1.5開始提供了AtomicStampedReference類來解决ABA問題,原子更新帶有版本號的引用類型。

  2. 循環時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
  3. 只能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能够保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。

    Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作。

並發工具

在JDK的並發包裏提供了幾個非常有用的並發工具類。CountDownLatch、CyclicBarrier和Semaphore工具類提供了一種並發流程控制的手段。

CountDownLatch

CountDownLatch用於某個線程等待其他線程執行完任務再執行,與thread.join()功能類似。常見的應用場景是開啟多個線程同時執行某個任務,等到所有任務執行完再執行特定操作,如匯總統計結果。

public class CountDownLatchDemo {
static final int N = 4;
static CountDownLatch latch = new CountDownLatch(N);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < N; i++) {
new Thread(new Thread1()).start();
}
latch.await(1000, TimeUnit.MILLISECONDS); //調用await()方法的線程會被掛起,它會等待直到count值為0才繼續執行;等待timeout時間後count值還沒變為0的話就會繼續執行
System.out.println("task finished");
}
static class Thread1 implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "starts working");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}
}

運行結果:

Thread-0starts working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task finished

CyclicBarrier

CyclicBarrier(同步屏障),用於一組線程互相等待到某個狀態,然後這組線程再同時執行。

public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

參數parties指讓多少個線程或者任務等待至某個狀態;參數barrierAction為當這些線程都達到某個狀態時會執行的內容。

public class CyclicBarrierTest {
// 請求的數量
private static final int threadCount = 10;
// 需要同步的線程數量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 創建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保證子線程完全執行結束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}

運行結果如下,可以看出CyclicBarrier是可以重用的:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

當四個線程都到達barrier狀態後,會從四個線程中選擇一個線程去執行Runnable。

CyclicBarrier和CountDownLatch區別

CyclicBarrier 和 CountDownLatch 都能够實現線程之間的等待。

CountDownLatch用於某個線程等待其他線程執行完任務再執行。CyclicBarrier用於一組線程互相等待到某個狀態,然後這組線程再同時執行。
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可用於處理更為複雜的業務場景。

Semaphore

Semaphore類似於鎖,它用於控制同時訪問特定資源的線程數量,控制並發線程數。

public class SemaphoreDemo {
public static void main(String[] args) {
final int N = 7;
Semaphore s = new Semaphore(3);
for(int i = 0; i < N; i++) {
new Worker(s, i).start();
}
}
static class Worker extends Thread {
private Semaphore s;
private int num;
public Worker(Semaphore s, int num) {
this.s = s;
this.num = num;
}
@Override
public void run() {
try {
s.acquire();
System.out.println("worker" + num + " using the machine");
Thread.sleep(1000);
System.out.println("worker" + num + " finished the task");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

運行結果如下,可以看出並非按照線程訪問順序獲取資源的鎖,即

worker0 using the machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 finished the task

原子類

基本類型原子類

使用原子的方式更新基本類型

  • 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 標記與引用關聯起來

給大家分享一個github倉庫,上面放了200多本經典的計算機書籍,包括C語言、C++、Java、Python、前端、數據庫、操作系統、計算機網絡、數據結構和算法、機器學習、編程人生等,可以star一下,下次找書直接在上面搜索,倉庫持續更新中~

github地址:https://github.com/Tyson0314/...

如果github訪問不了,可以訪問gitee倉庫。

gitee地址:https://gitee.com/tysondai/ja...

Java並發

大家好,我是大彬。最近在面試,看了很多面經,將常見的Java並發編程常見面試題總結了一下,如果對你有幫助,可以 收藏和點贊後續還會繼續更新新的面試題目哦!

文章目錄如下:

線程池

線程池:一個管理線程的池子。

為什麼使用線程池?

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷毀造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。統一管理線程,避免系統創建大量同類線程而導致消耗完內存。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

線程池執行原理?

創建新的線程需要獲取全局鎖,通過這種設計可以盡量避免獲取全局鎖,當 ThreadPoolExecutor 完成預熱之後(當前運行的線程數大於等於 corePoolSize),提交的大部分任務都會被放到 BlockingQueue。

為了形象描述線程池執行,打個比喻:

  • 核心線程比作公司正式員工
  • 非核心線程比作外包員工
  • 阻塞隊列比作需求池
  • 提交任務比作提需求

線程池參數有哪些?

ThreadPoolExecutor 的通用構造函數:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:當有新任務時,如果線程池中線程數沒有達到線程池的基本大小,則會創建新的線程執行任務,否則將任務放入阻塞隊列。當線程池中存活的線程數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
  • maximumPoolSize:當阻塞隊列填滿時,如果線程池中線程數沒有超過最大線程數,則會創建新的線程運行任務。否則根據拒絕策略處理新任務。非核心線程類似於臨時借來的資源,這些線程在空閑時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
  • BlockingQueue:存儲等待運行的任務。
  • keepAliveTime:非核心線程空閑後,保持存活的時間,此參數只對非核心線程有效。設置為0,錶示多餘的空閑線程會被立即終止。
  • TimeUnit:時間單比特

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
  • 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);//將線程池名字傳遞給構造函數,用於區分不同線程池的線程
    }
    }
  • RejectedExecutionHandler:當隊列和線程池都滿了時,根據拒絕策略處理新任務。

    AbortPolicy:默認的策略,直接拋出RejectedExecutionException
    DiscardPolicy:不處理,直接丟弃
    DiscardOldestPolicy:將等待隊列隊首的任務丟弃,並執行當前任務
    CallerRunsPolicy:由調用線程處理該任務

線程池大小怎麼設置?

如果線程池線程數量太小,當有大量請求需要處理,系統響應比較慢影響體驗,甚至會出現任務隊列大量堆積任務導致OOM。

如果線程池線程數量過大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換(cpu給線程分配時間片,當線程的cpu時間片用完後保存狀態,以便下次繼續運行),從 而增加線程的執行時間,影響了整體執行效率。

CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是為了防止某些原因導致的任務暫停(線程阻塞,如io操作,等待鎖,線程sleep)而帶來的影響。一旦某個線程被阻塞,釋放了cpu資源,而在這種情况下多出來的一個線程就可以充分利用 CPU 的空閑時間。

I/O 密集型任務(2N): 系統會用大部分的時間來處理 I/O 操作,而線程等待 I/O 操作會被阻塞,釋放 cpu資源,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法:最佳線程數 = CPU核心數 (1/CPU利用率) = CPU核心數 (1 + (I/O耗時/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 封裝了一個 PriorityQueuePriorityQueue 會對隊列中的任務進行排序,時間早的任務先被執行(即ScheduledFutureTasktime 變量小的先執行),如果time相同則先提交的任務會被先執行(ScheduledFutureTasksquenceNumber 變量小的先執行)。

執行周期任務步驟:

  1. 線程從 DelayQueue 中獲取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任務是指 ScheduledFutureTask的 time 大於等於當前系統的時間;
  2. 執行這個 ScheduledFutureTask
  3. 修改 ScheduledFutureTask 的 time 變量為下次將要被執行的時間;
  4. 把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

適用場景:周期性執行任務的場景,需要限制線程數量的場景。

進程線程

進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,一個進程中可以啟動多個線程。
線程是比進程更小的執行單比特,它是在一個進程中獨立的控制流,一個進程可以啟動多個線程,每條線程並行執行不同的任務。

線程的生命周期

初始(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類所具有的優勢:

  1. 資源共享,適合多個相同的程序代碼的線程去處理同一個資源
  2. 可以避免java中的單繼承的限制
  3. 線程池只能放入實現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() 方法,無法達到啟動多線程的目的,相當於主線程線性執行 Thread 對象的 run() 方法。
一個線程對線的 start() 方法只能調用一次,多次調用會拋出 java.lang.IllegalThreadStateException 异常;run() 方法沒有限制。

線程都有哪些方法?

join

Thread.join(),在main中創建了thread線程,在main中調用了thread.join()/thread.join(long millis),main線程放弃cpu控制權,線程進入WAITING/TIMED_WAITING狀態,等到thread線程執行完才繼續執行main線程。

public final void join() throws InterruptedException {
join(0);
}

yield

Thread.yield(),一定是當前線程調用此方法,當前線程放弃獲取的CPU時間片,但不釋放鎖資源,由運行狀態變為就緒狀態,讓OS再次選擇線程。作用:讓相同優先級的線程輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由用戶指定暫停多長時間。

public static native void yield(); //static方法

sleep

Thread.sleep(long millis),一定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,讓出cpu資源,但不釋放對象鎖,指定時間到後又恢複運行。作用:給其它線程執行機會的最佳方式。

public static native void sleep(long millis) throws InterruptedException;//static方法

volatile底層原理

volatile是輕量級的同步機制,volatile保證變量對所有線程的可見性,不保證原子性。

  1. 當對volatile變量進行寫操作的時候,JVM會向處理器發送一條LOCK前綴的指令,將該變量所在緩存行的數據寫回系統內存。
  2. 由於緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己的緩存是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行置為無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存中。

MESI(緩存一致性協議):當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,就會從內存重新讀取。

volatile關鍵字的兩個作用:

  1. 保證了不同線程對共享變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
  2. 禁止進行指令重排序。

指令重排序是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高並行度。Java編譯器會在生成指令系列時在適當的比特置會插入內存屏障指令來禁止處理器重排序。插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。對一個volatile字段進行寫操作,Java內存模型將在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都刷新到內存。

AQS原理

AQS,AbstractQueuedSynchronizer,抽象隊列同步器,定義了一套多線程訪問共享資源的同步器框架,許多並發工具的實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一個volatile的int類型的成員變量state來錶示同步狀態,通過CAS修改同步狀態的值。當線程調用 lock 方法時 ,如果 state=0,說明沒有任何線程占有共享資源的鎖,可以獲得鎖並將 state=1。如果 state=1,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待。

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

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態(獨占或共享 )構造成為一個節點(Node)並將其加入同步隊列並進行自旋,當同步狀態釋放時,會把首節中的後繼節點對應的線程喚醒,使其再次嘗試獲取同步狀態。

synchronized的用法有哪些?

  1. 修飾普通方法:作用於當前對象實例,進入同步代碼前要獲得當前對象實例的鎖
  2. 修飾靜態方法:作用於當前類,進入同步代碼前要獲得當前類對象的鎖,synchronized關鍵字加到static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖
  3. 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖

Synchronized的作用有哪些?

原子性:確保線程互斥的訪問同步代碼;
可見性:保證共享變量的修改能够及時可見,其實是通過Java內存模型中的 “對一個變量unlock 操作之前,必須要同步到主內存中;如果對一個變量進行lock操作,則將會清空工作內存中此變量的值,在執行引擎使用此變量前,需要重新從主內存中load操作或assign操作初始化變量值” 來保證的;
有序性:有效解决重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”。

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 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。

ReentrantLock 是如何實現可重入性的?

ReentrantLock 內部自定義了同步器 Sync,在加鎖的時候通過 CAS 算法,將線程對象放到一個雙向鏈錶中,每次獲取鎖的時候,檢查當前維護的那個線程 ID 和當前請求的線程 ID 是否 一致,如果一致,同步狀態加1,錶示鎖被當前線程獲取了多次。

ReentrantLock和synchronized區別

  1. 使用synchronized關鍵字實現同步,線程執行完同步代碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
  2. synchronized是非公平鎖,ReentrantLock可以設置為公平鎖。
  3. ReentrantLock上等待獲取鎖的線程是可中斷的,線程可以放弃等待鎖。而synchonized會無限期等待下去。
  4. ReentrantLock 可以設置超時獲取鎖。在指定的截止時間之前獲取鎖,如果截止時間到了還沒有獲取到鎖,則返回。
  5. ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的獲取鎖,調用該方法後立刻返回,如果能够獲取則返回true,否則返回false。

wait()和sleep()的區別

相同點:

  1. 使當前線程暫停運行,把機會交給其他線程
  2. 任何線程在等待期間被中斷都會拋出InterruptedException

不同點:

  1. wait() 是Object超類中的方法;而sleep()是線程Thread類中的方法
  2. 對鎖的持有不同,wait()會釋放鎖,而sleep()並不釋放鎖
  3. 喚醒方法不完全相同,wait() 依靠notify或者notifyAll 、中斷、達到指定時間來喚醒;而sleep()到達指定時間被喚醒
  4. 調用obj.wait()需要先獲取對象的鎖,而 Thread.sleep()不用

wait(),notify()和suspend(),resume()之間的區別

  • wait() 使得線程進入阻塞等待狀態,並且釋放鎖
  • notify()喚醒一個處於等待狀態的線程,它一般跟wait()方法配套使用。
  • suspend()使得線程進入阻塞狀態,並且不會自動恢複,必須對應的resume() 被調用,才能使得線程重新進入可執行狀態。suspend()方法很容易引起死鎖問題。
  • resume()方法跟suspend()方法配套使用。

suspend()不建議使用,suspend()方法在調用後,線程不會釋放已經占有的資 源(比如鎖),而是占有著資源進入睡眠狀態,這樣容易引發死鎖問題。

Runnable和 Callable有什麼區別?

  • Callable接口方法是call(),Runnable的方法是run();
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法無返回值。
  • Callable接口call()方法允許拋出异常;而Runnable接口run()方法不能繼續上拋异常;

volatile和synchronized的區別是什麼?

  1. volatile只能使用在變量上;而synchronized可以在類,變量,方法和代碼塊上。
  2. volatile至保證可見性;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序;synchronized不會。
  4. volatile不會造成阻塞;synchronized會。

線程執行順序怎麼控制?

假設有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次

樂觀鎖一定就是好的嗎?

樂觀鎖避免了悲觀鎖獨占對象的現象,提高了並發性能,但它也有缺點:

  • 樂觀鎖只能保證一個共享變量的原子操作。如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解决,不管對象數量多少及對象顆粒度大小。
  • 長時間自旋可能導致開銷大。假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷。
  • ABA 問題。CAS 的核心思想是通過比對內存值與預期值是否一樣而判 斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是 A, 後來被一條線程改為 B,最後又被改成了 A,則 CAS 認為此內存值並 沒有發生改變,但實際上是有被其他線程改過的,這種情况對依賴過程值的情景的運算結果影響很大。解决的思路是引入版本號,每次變量更新都把版本號加一。

守護線程是什麼?

守護線程是運行在後臺的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些 發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。

線程間通信方式

volatile

volatile是輕量級的同步機制,volatile保證變量對所有線程的可見性,不保證原子性。

synchronized

保證線程對變量訪問的可見性和排他性。

等待通知機制

wait/notify為 Object 對象的方法,調用wait/notify需要先獲得對象的鎖。對象調用wait之後線程釋放鎖,將線程放到對象的等待隊列,當通知線程調用此對象的notify()方法後,等待線程並不會立即從wait返回,需要等待通知線程釋放鎖(通知線程執行完同步代碼塊),等待隊列裏的線程獲取鎖,獲取鎖成功才能從wait()方法返回,即從wait方法返回前提是線程獲得鎖。

等待通知機制依托於同步機制,目的是確保等待線程從wait方法返回時能感知到通知線程對對象的變量值的修改。

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內存泄漏的原因?

每個Thread都有⼀個ThreadLocalMap的內部屬性,map的key是ThreaLocal,定義為弱引用,value是强引用類型。GC的時候會⾃動回收key,而value的回收取决於Thread對象的生命周期。一般會通過線程池的方式複用Thread對象節省資源,這也就導致了Thread對象的生命周期比較長,這樣便一直存在一條强引用鏈的關系:Thread --> ThreadLocalMap-->Entry-->Value,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致內存泄漏。

image-20200715235804982

解决⽅法:每次使⽤完ThreadLocal就調⽤它的remove()⽅法,手動將對應的鍵值對删除,從⽽避免內存泄漏。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal使用場景有哪些?

ThreadLocal 適用場景:每個線程需要有自己單獨的實例,且需要在多個方法中共享實例,即同時滿足實例在線程間的隔離與方法間的共享。比如Java web應用中,每個線程有自己單獨的 Session 實例,就可以使用ThreadLocal來實現。

鎖的分類

公平鎖與非公平鎖

按照線程訪問順序獲取對象鎖。synchronized 是非公平鎖, Lock 默認是非公平鎖,可以設置為公平鎖,公平鎖會影響性能。

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

共享式與獨占式鎖

共享式與獨占式的最主要區別在於:同一時刻獨占式只能有一個線程獲取同步狀態,而共享式在同一時刻可以有多個線程獲取同步狀態。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作,其他操作都會被阻塞。

悲觀鎖與樂觀鎖

悲觀鎖,每次訪問資源都會加鎖,執行完同步代碼釋放鎖,synchronized 和 ReentrantLock 屬於悲觀鎖。

樂觀鎖,不會鎖定資源,所有的線程都能訪問並修改同一個資源,如果沒有沖突就修改成功並退出,否則就會繼續循環嘗試。樂觀鎖最常見的實現就是CAS。

樂觀鎖一般來說有以下2種方式:

  1. 使用數據版本記錄機制實現,這是樂觀鎖最常用的一種實現方式。給數據增加一個版本標識,一般是通過為數據庫錶增加一個數字類型的version字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫錶對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫錶當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。
  2. 使用時間戳。數據庫錶增加一個字段,字段類型使用時間戳(timestamp),和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。

適用場景:

  • 悲觀鎖適合寫操作多的場景。
  • 樂觀鎖適合讀操作多的場景,不加鎖可以提昇讀操作的性能。

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內的valueexpect相等,就證明沒有其他線程改變過這個變量,那麼就更新它為update,如果不相等,那就會繼續重試直到成功更新值。

CAS存在的問題?

CAS 三大問題:

  1. ABA問題。CAS需要在操作值的時候檢查內存值是否發生變化,沒有發生變化才會更新內存值。但是如果內存值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解决思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從A-B-A變成了1A-2B-3A

    JDK從1.5開始提供了AtomicStampedReference類來解决ABA問題,原子更新帶有版本號的引用類型。

  2. 循環時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
  3. 只能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能够保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。

    Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作。

並發工具

在JDK的並發包裏提供了幾個非常有用的並發工具類。CountDownLatch、CyclicBarrier和Semaphore工具類提供了一種並發流程控制的手段。

CountDownLatch

CountDownLatch用於某個線程等待其他線程執行完任務再執行,與thread.join()功能類似。常見的應用場景是開啟多個線程同時執行某個任務,等到所有任務執行完再執行特定操作,如匯總統計結果。

public class CountDownLatchDemo {
static final int N = 4;
static CountDownLatch latch = new CountDownLatch(N);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < N; i++) {
new Thread(new Thread1()).start();
}
latch.await(1000, TimeUnit.MILLISECONDS); //調用await()方法的線程會被掛起,它會等待直到count值為0才繼續執行;等待timeout時間後count值還沒變為0的話就會繼續執行
System.out.println("task finished");
}
static class Thread1 implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "starts working");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}
}

運行結果:

Thread-0starts working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task finished

CyclicBarrier

CyclicBarrier(同步屏障),用於一組線程互相等待到某個狀態,然後這組線程再同時執行。

public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

參數parties指讓多少個線程或者任務等待至某個狀態;參數barrierAction為當這些線程都達到某個狀態時會執行的內容。

public class CyclicBarrierTest {
// 請求的數量
private static final int threadCount = 10;
// 需要同步的線程數量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 創建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保證子線程完全執行結束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}

運行結果如下,可以看出CyclicBarrier是可以重用的:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

當四個線程都到達barrier狀態後,會從四個線程中選擇一個線程去執行Runnable。

CyclicBarrier和CountDownLatch區別

CyclicBarrier 和 CountDownLatch 都能够實現線程之間的等待。

CountDownLatch用於某個線程等待其他線程執行完任務再執行。CyclicBarrier用於一組線程互相等待到某個狀態,然後這組線程再同時執行。
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可用於處理更為複雜的業務場景。

Semaphore

Semaphore類似於鎖,它用於控制同時訪問特定資源的線程數量,控制並發線程數。

public class SemaphoreDemo {
public static void main(String[] args) {
final int N = 7;
Semaphore s = new Semaphore(3);
for(int i = 0; i < N; i++) {
new Worker(s, i).start();
}
}
static class Worker extends Thread {
private Semaphore s;
private int num;
public Worker(Semaphore s, int num) {
this.s = s;
this.num = num;
}
@Override
public void run() {
try {
s.acquire();
System.out.println("worker" + num + " using the machine");
Thread.sleep(1000);
System.out.println("worker" + num + " finished the task");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

運行結果如下,可以看出並非按照線程訪問順序獲取資源的鎖,即

worker0 using the machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 finished the task

原子類

基本類型原子類

使用原子的方式更新基本類型

  • 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 標記與引用關聯起來

小夥伴們覺得有用的話,點贊加收藏,支持一下!

版权声明:本文为[程序員大彬]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210919122246788C.html