java不同線程解讀以及線程池的使用方式
java不同線程解讀以及線程池的使用
線程池子類比一個水池,而每一個線程,都好比水池的水。因此,水池多大,看系統(tǒng)硬件配置。線程池主要為了并發(fā),高效處理任務。而異步處理任務,可以有效提高處理任務的吞吐量。
線程池的常見應用場景
處理大量而短小的請求,請求數量很大,每一個請求開啟一個線程,在請求完畢之后再對線程進行銷毀,這樣創(chuàng)建和銷毀線程所消耗的時間往往比任務本身所需消耗的資源要大得多。
線程池做到線程復用,不需要頻繁的創(chuàng)建和銷毀線程,線程池中的線程一直存在于線程池中,線程從任務隊列中取得任務來執(zhí)行。而且這樣做的另一個好處有,通過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到獲得一個線程來處理為止,從而可以防止資源不足。
線程池是什么?
線程池用于多線程處理中,它可以根據系統(tǒng)的情況,可以控制線程執(zhí)行的數量,優(yōu)化運行效果。線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然后在線程創(chuàng)建后啟動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等候,等其它線程執(zhí)行完畢,再從隊列中取出任務來執(zhí)行。
線程池的作用
創(chuàng)建對象和銷毀對象是非常消耗時間和資源的。因此想要最小化這種消耗的一種思想就是『池化資源』,通過重用線程池中的資源來減少創(chuàng)建和銷毀線程所需要耗費的時間和資源。
線程池的一個作用是創(chuàng)建和銷毀線程的次數,每個工作線程可以多次使用;另一個作用是可根據系統(tǒng)情況調整執(zhí)行的線程數量,防止消耗過多內存。另外,通過線程池,能有效的控制線程的最大并發(fā)數,提高系統(tǒng)資源利用率,同時避免過多的資源競爭,避免堵塞。
線程池的優(yōu)點總結如下幾個方面:
- 線程復用
- 控制最大并發(fā)數
- 管理線程
線程池組成:
- 線程池管理器:用于創(chuàng)建并管理線程池
- 工作線程:線程池中的線程
- 任務接口:每個任務必須實現的接口,用于工作線程調度其運行
- 任務隊列:用于存放待處理的任務,提供一種緩沖機制
1、Async 和 @Async 很關鍵
兩個注解很關鍵:
- @EnableAsync 啟用基于異步方法的執(zhí)行
- @Async 這注解的函數會被異步處理
2、Thread和Runnable
Thread可以創(chuàng)建線程,但是線程使用完之后需要對線程資源進行銷毀回收,消耗資源,且容易造成線程上下文切換(操作系統(tǒng)核心對CPU上對進程或者線程進行切換)問題,線程管理不當容易資源耗盡,不建議使用。、
一個類實現 Runnable 接口可以將該類的實例傳遞給一個 Thread 對象并啟動一個新線程,這個新線程就會執(zhí)行該類中 void run() 方法中所定義的邏輯。
3、數據類型—線程安全
如果有多線程共享數據的方式,要牢記各種使用場景的線程安全數據類型。
- (1)、AtomicInteger 原子int整型;
- (2)、AtomicLong 原子long整型;
- (3)、AtomicBoolean 原子boolean;
- (4)、List 這三種都是線程安全型:
①List<T> vector = new Vector<>(); ②<T> listSyn = Collections.synchronizedList(new ArrayList<>()); ③List<T> copyList = new CopyOnWriteArrayList<>();
4、@Configuration配置類和 @Bean將實例對象交給IOC容器
5、ThreadPoolExecutor 和 ThreadPoolTaskExecutor
兩種線程池方式,本質上一樣,ThreadPoolTaskExecutor(我使用的) 源碼上是在 ThreadPoolExecutor 上再加了一層包裝,為了更方便在spring框架中使用。
- 配置類: 可以確保異步執(zhí)行配置對應用中的所有 bean 都生效。
- 啟動類: 啟動類上整個應用中啟用異步
- 推薦: 針對應用中的不同部分提供不同的異步執(zhí)行策略,或者只需要特定的一部分 bean 具備異步執(zhí)行能力,放配置類上。如果整個應用都需要異步支持,放置在啟動類。
使用@Bean(“beanName”)定義線程池 然后在@Async(“beanName”)中引用指定的線程池
@EnableAsync
用于啟用整個應用程序的異步處理功能,包括所有通過 @Async
注解標記的方法。它不負責配置底層線程池。
ThreadPoolTaskExecutor
Bean 配置則是用于配置具體的線程池實例,這個線程池會被 @Async
方法所使用。
如果你在配置類中同時使用了 @EnableAsync
注解和自定義的 ThreadPoolTaskExecutor
Bean 配置,Spring 將會使用你配置的線程池執(zhí)行異步方法。如果你只使用 @EnableAsync
,Spring 將會使用默認的線程池配置。
注意:
- 1、除了要在方法上加@Async注解,還需要在啟動類加注解@EnableAsync啟動多線程注解,@Async就會對標注的方法開啟異步多線程調用,注意,這個方法的類一定要交給Spring容器來管理。
- 2、法一定要從另一個類中調用,也就是從類的外部調用,類的內部調用是無效的,因為@Transactional和@Async注解的實現都是基于Spring的AOP,而AOP的實現是基于動態(tài)代理模式實現的。那么注解失效的原因就很明顯了,有可能因為調用方法的是對象本身而不是代理對象,因為沒有經過Spring容器
- 3、異步方法使用注解@Async的返回值只能為void或者Future
- 4、方法必須是public方法
定義線程池的配置
package com.test.wll.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; @EnableAsync//或者加在app類上,此處不加需要在啟動類上加 @Configuration public class ThreadPoolTaskConfig { @Bean("threadPoolRedisTaskExecutor") public ThreadPoolTaskExecutor threadPoolRedisTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //線程池創(chuàng)建的核心線程數,線程池維護線程的最少數量,即使沒有任務需要執(zhí)行,也會一直存活 executor.setCorePoolSize(8); //如果設置allowCoreThreadTimeout=true(默認false)時,核心線程會超時關閉 //executor.setAllowCoreThreadTimeOut(true); //阻塞隊列 當核心線程數達到最大時,新任務會放在隊列中排隊等待執(zhí)行 executor.setQueueCapacity(124); //最大線程池數量,當線程數>=corePoolSize,且任務隊列已滿時。線程池會創(chuàng)建新線程來處理任 //任務隊列已滿時, 且當線程數=maxPoolSize,,線程池會拒絕處理任務而拋出異常 executor.setMaxPoolSize(64); //當線程空閑時間達到keepAliveTime時,線程會退出,直到線程數量=corePoolSize //允許線程空閑時間30秒,當maxPoolSize的線程在空閑時間到達的時候銷毀 //如果allowCoreThreadTimeout=true,則會直到線程數量=0 executor.setKeepAliveSeconds(30); //spring 提供的 ThreadPoolTaskExecutor 線程池,是有setThreadNamePrefix() 方法的。 //jdk 提供的ThreadPoolExecutor 線程池是沒有 setThreadNamePrefix() 方法的 executor.setThreadNamePrefix("threadPoolRedisTaskExecutor"); // rejection-policy:拒絕策略:當線程數已經達到maxSize的時候,如何處理新任務 // CallerRunsPolicy():交由調用方線程運行,比如 main 線程;如果添加到線程池失敗,那么主線程會自己去執(zhí)行該任務,不會等待線程池中的線程去執(zhí)行 // AbortPolicy():該策略是線程池的默認策略,如果線程池隊列滿了丟掉這個任務并且拋出RejectedExecutionException異常。 // DiscardPolicy():如果線程池隊列滿了,會直接丟掉這個任務并且不會有任何異常 // DiscardOldestPolicy():丟棄隊列中最老的任務,隊列滿了,會將最早進入隊列的任務刪掉騰出空間,再嘗試加入隊列 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); executor.initialize(); return executor; } } } //最大線程數 executor.setMaxPoolSize(maxPoolSize); //核心線程數 executor.setCorePoolSize(corePoolSize); //任務隊列的大小 executor.setQueueCapacity(queueCapacity); //線程前綴名 executor.setThreadNamePrefix(namePrefix); //線程存活時間 executor.setKeepAliveSeconds(keepAliveSeconds);
//此處的名字要與線程池的名字一致,清晰的命名可以清晰的了解哪個線程報錯 @Async("threadName") @GetMapping("/方法名字") public Result<String> test() { }
6、拒絕策略
rejectedExectutionHandler參數字段用于配置絕策略,常用拒絕策略如下
AbortPolicy
:用于被拒絕任務的處理程序,它將拋出RejectedExecutionExceptionCallerRunsPolicy
:用于被拒絕任務的處理程序,直接在execute方法的調用線程中運行被拒絕的任務。DiscardOldestPolicy
:用于被拒絕任務的處理程序,放棄最舊的未處理請求,然后重試execute。DiscardPolicy
:用于被拒絕任務的處理程序,默認情況下它將丟棄被拒絕的任務。
7、線程池處理流程
線程池的工作流程(必知必會)?
這個問題回答的時候,最好用講故事的方式進行。 假如核心線程數是5,最大線程數是10,阻塞隊列也是10
- 1)有新任務來的時候,將先使用核心線程執(zhí)行;
- 2)當任務數達到5個的時候,第6個任務開始排隊;
- 3)當任務數達到15個的時候,第16個任務將開啟新的線程執(zhí)行,也就是第6個線程
- 4)當任務數達到20個的時候,線程池滿了,如果有第21個任務,將執(zhí)行拒絕策略
8、 Hutool 的ThreadUtil.execute
execute
方法是通過 GlobalThreadPool
來執(zhí)行任務的,而 GlobalThreadPool
在 Hutool 中是一個全局的線程池。
它會使用默認的配置來初始化線程池,這些默認配置在 Hutool 內部已經設定好了,因此在使用 execute
方法時不需要手動指定核心池大小和最大池大小。
這種方法適合簡單的任務執(zhí)行,如果需要更靈活的線程池配置(比如自定義線程數、隊列類型等),可以考慮使用 newExecutor
等方法手動創(chuàng)建線程池,并進行更詳細的配置。
spring的 ThreadPoolTaskExecutor
:
- 優(yōu)點: Spring 的
ThreadPoolTaskExecutor
是 Spring 框架提供的一個線程池實現,它提供了很多配置選項,允許你更加靈活地定制線程池的行為,比如核心線程數、最大線程數、隊列容量、線程存活時間等。 - 適用情況: 適合在 Spring 環(huán)境中使用,對于基于 Spring 的項目,使用這種方式可以充分利用 Spring 提供的特性和管理能力,例如可以方便地集成到 Spring 的任務調度中
Hutool ThreadUtil
:
- 優(yōu)點: Hutool 提供的
ThreadUtil
類是一個簡化了的工具類,提供了一些靜態(tài)方法來方便地創(chuàng)建線程池和執(zhí)行任務。它是一個輕量級的工具庫,適合在一般的 Java 程序中使用,不依賴于 Spring 框架。 - 適用情況: 適合非 Spring 項目或者不需要集成到 Spring 容器管理的場景。如果你不需要太多的線程池配置選項,而只是簡單地執(zhí)行任務,使用
ThreadUtil
的execute
方法會更加便捷。
常見線程池
- 1)定長線程池(FixedThreadPool)
- 2)定時線程池(ScheduledThreadPool)
- 3)可緩存線程池(CachedThreadPool)
- 4)單線程化線程池(SingleThreadExecutor)
核心概念:這四個線程池的本質都是ThreadPoolExecutor對象(看源碼)
不同點在于:
- 1)FixedThreadPool:只有核心線程,線程數量固定,執(zhí)行完立即回收,任務隊列為鏈表結構的有界隊列。
- 2)ScheduledThreadPool:核心線程數量固定,非核心線程數量無限,執(zhí)行完閑置 10ms 后回收,任務隊列為延時阻塞隊列。
- 3)CachedThreadPool:無核心線程,非核心線程數量無限,執(zhí)行完閑置 60s 后回收,任務隊列為不存儲元素的阻塞隊列。
- 4)SingleThreadExecutor:只有 1 個核心線程,無非核心線程,執(zhí)行完立即回收,任務隊列為鏈表結構的有界隊列
線程池的主要參數有哪些(必知必會)?
- 1)corePoolSize(必需):核心線程數。默認情況下,核心線程會一直存活,但是當將 allowCoreThreadTimeout 設置為 true 時,核心線程也會超時回收。
- 2)maximumPoolSize(必需):線程池所能容納的最大線程數。當活躍線程數達到該數值后,后續(xù)的新任務將會阻塞。
- 3)keepAliveTime(必需):線程閑置超時時長。如果超過該時長,非核心線程就會被回收。如果將 allowCoreThreadTimeout 設置為 true 時,核心線程也會超時回收。
- 4)unit(必需):指定 keepAliveTime 參數的時間單位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- 5)workQueue(必需):任務隊列。通過線程池的 execute() 方法提交的 Runnable 對象將存儲在該參數中。其采用阻塞隊列實現。
- 6)threadFactory(可選):線程工廠。用于指定為線程池創(chuàng)建新線程的方式。
- 7)handler(可選):拒絕策略。當達到最大線程數時需要執(zhí)行的飽和策略。
線程池的工作流程(必知必會)?
這個問題回答的時候,最好用講故事的方式進行。 假如核心線程數是5,最大線程數是10,阻塞隊列也是10
- 1)有新任務來的時候,將先使用核心線程執(zhí)行;
- 2)當任務數達到5個的時候,第6個任務開始排隊;
- 3)當任務數達到15個的時候,第16個任務將開啟新的線程執(zhí)行,也就是第6個線程
- 4)當任務數達到20個的時候,線程池滿了,如果有第21個任務,將執(zhí)行拒絕策略
線程安全
函數、函數庫在并發(fā)環(huán)境中被調用時,能夠正確地處理多個線程之間的共享變量,使程序功能正確完成。
- 并發(fā):(由cpu分配時間片,看似像同時干多個事情實際是由不同的cpu時間片干)任務執(zhí)行時間比調度頻率長,會導致任務堆積,任務完成后,才能開始下一個任務,可能會造成任務被阻塞,直到有空閑線程可用。當任務執(zhí)行時間比調度頻率長時,會出現并發(fā)問題,線程池中的線程會被任務堵塞,直到前一個任務執(zhí)行完成。
- 并行:當系統(tǒng)有一個以上CPU時,當一個CPU執(zhí)行一個進程時,另一個CPU可以執(zhí)行另一個進程,兩個進程互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel)。
eg:并發(fā)是兩個隊伍交替使用一臺咖啡機。并行是兩個隊伍同時使用兩臺咖啡機。
- 線程和進程:下載為進程,下載的多個視頻為線程,共享線程資源
- 共享變量:指的是多個線程都可以操作的變量
保存在堆
和方法區(qū)
中的變量就是Java中的共享變量
堆中存放new出來的對象,(包括實例變量); 棧中存放正在調用的方法中的局部變量 (包括方法的參數); 方法區(qū)中存儲.class 字節(jié)碼 文件(包括 靜態(tài)變量 、靜態(tài)方法)
public class Variables { // 類變量 共享變量 private static int a; //成員變量 共享變量 private int b; //局部變量 c和d 非共享變量 public void test(int c){ int d; } } //多線程場景,對于變量a和b的操作是需要考慮線程安全的,而對于線程c和d的操作是不需要考慮線程安全的。
線程不安全
多線程并發(fā)執(zhí)行某個代碼時,產生了邏輯上的錯誤,結果和預期值不相同 線程安全是指多線程執(zhí)行時沒有產生邏輯錯誤,結果和預期值相同
//開啟了兩個線程,每個線程執(zhí)行1000次循環(huán),循環(huán)中對count進行加1操作。等待兩個線程都執(zhí)行完成后,打印count的值。 public class Test { private static int count; private static class Thread1 extends Thread { public void run() { for (int i = 0; i < 1000; i++) { count ++; try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread1 t1 = new Thread1(); Thread1 t2 = new Thread1(); t1.start(); t2.start(); //main主線程內調用join()方法:休眠主線程,等待t1、t2線程執(zhí)行完畢主線程再繼續(xù),即最后輸出count值 t1.join(); t2.join(); System.out.println(count); } }
輸出結果應該是2000但是不有時候1996
count++的指令在實際執(zhí)行的過程中不是原子性的,而是要分為讀、改、寫三步來進行;即先從內存中讀出count的值,然后執(zhí)行+1操作,再將結果寫回內存中,如下圖所示。
上圖中線程1執(zhí)行了兩次自加操作,而線程2執(zhí)行了一次自加操作,但是count卻從6變成了8,只加了2。
我們看一下為什么會出現這種情況。當線程1讀取count的值為6完成后,此時切換到了線程2執(zhí)行,線程2同樣讀取到了count的值為6,而后進行改和寫操作,count的值變?yōu)榱?;此時線程又切回了線程1,但是線程1中count的值依然是線程2修改前的6,這就是問題所在!即線程2修改了count的值,但是這種修改對線程1不可見,導致了程序出現了線程不安全的問題,沒有符合我們預期的邏輯。
導致線程不安全的原因
主要有三點:
- 不滿足原子性:一個或者多個操作在 CPU 執(zhí)行的過程中被中斷,當 cpu 執(zhí)行一個線程過程時,調度器可能調走CPU,去執(zhí)行另一個線程,此線程的操作可能還沒有結束;(synchronized鎖解決)
- 不滿足可見性:一個線程對共享變量的修改,另外一個線程不能立刻看到
- 不滿足有序性:程序執(zhí)行的順序沒有按照代碼的先后順序執(zhí)行三、怎樣解決線程不安全
Java中的原子操作包括:
- 除long和double之外的基本類型的賦值操作
- 所有引用reference的賦值操作
- java.concurrent.Atomic.* 包中所有類的一切操作
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
volatile
關鍵字來保證可見性
。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
volatile
關鍵字來保證一定的“有序性”
。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。另外,Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
補充:happens-before原則(先行發(fā)生原則)
- 程序次序規(guī)則:一個線程內,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作。
- volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
- 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C。
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作。
- 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。
- 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執(zhí)行。
- 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始。
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
解決maven打包排除類不生效maven-compiler-plugin問題
總結:在Spring Boot項目B中作為項目A的依賴時,排除啟動類不生效的原因是被其他類引用或父POM引入,解決方法是跳過test編譯或注釋掉@SpringBootTest(classes={BApplication.class})2024-11-11區(qū)分java中String+String和String+char
這篇文章主要向大家詳細區(qū)分了java中String+String和String+char,感興趣的小伙伴們可以參考一下2016-01-01java9新特性Collection集合類的增強與優(yōu)化方法示例
這篇文章主要為大家介紹了java9新特性Collection集合類的增強與優(yōu)化方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03SpringBoot+Docker+IDEA實現一鍵構建+推送、運行、同鏡像多容器啟動
這篇文章主要介紹了SpringBoot+Docker+IDEA實現一鍵構建+推送、運行、同鏡像多容器啟動,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04