Java中的異步與線程池解讀
初始化線程的4種方式
1.繼承Thread
Thread01 thread01 = new Thread01(); thread01.start(); public static class Thread01 extends Thread{ @Override public void run() { System.out.println("當(dāng)前線程:"+Thread.currentThread().getId()); int i = 10 / 2; System.out.println("運(yùn)行結(jié)果:"+i); } }
2.實(shí)現(xiàn)Runnable 接口
Runnable01 runnable01 = new Runnable01(); new Thread(runnable01).start(); public static class Runnable01 implements Runnable{ @Override public void run() { System.out.println("當(dāng)前線程:"+Thread.currentThread().getId()); int i = 10 / 2; System.out.println("運(yùn)行結(jié)果:"+i); } }
3.實(shí)現(xiàn)Callable 接口+ FutureTask (可以拿到返回結(jié)果,可以處理異常)
Callabel01 callabel01 = new Callabel01(); FutureTask<Integer> integerFutureTask = new FutureTask<>(callabel01); //阻塞等待整個(gè)線程執(zhí)行完成,獲取返回結(jié)果 Integer integer = integerFutureTask.get(); new Thread(integerFutureTask).start(); public static class Callabel01 implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("當(dāng)前線程:"+Thread.currentThread().getId()); int i = 10 / 2; System.out.println("運(yùn)行結(jié)果:"+i); return i; } }
在業(yè)務(wù)代碼里面不建議使用以上三種啟動線程的方式
4.線程池
應(yīng)該將所有的多線程異步任務(wù)都交給線程池執(zhí)行,進(jìn)行有效的資源控制
//當(dāng)前系統(tǒng)中池只有一兩個(gè),每一個(gè)異步任務(wù)直接提交給線程池,讓他自己去執(zhí)行 ExecutorService service = Executors.newFixedThreadPool(10); //執(zhí)行 service.execute(new Runnable01());
區(qū)別
1/2兩種方式都不能獲取返回值
1/2/3都不能達(dá)到資源控制的效果
只有4能控制資源,系統(tǒng)性能是穩(wěn)定的
創(chuàng)建線程池(ExecutorService)
1.Executors 工具類創(chuàng)建
//當(dāng)前系統(tǒng)中池只有一兩個(gè),每一個(gè)異步任務(wù)直接提交給線程池,讓他自己去執(zhí)行 ExecutorService service = Executors.newFixedThreadPool(10); //執(zhí)行 service.execute(new Runnable01());
2.原生方法創(chuàng)建線程池
ThreadPoolExecutor需要傳入七大參數(shù)
corePoolSize
核心線程數(shù)【一直存在,除非設(shè)置了允許線程超時(shí)的設(shè)置:allowCoreThreadTimeOut】,保留在池中的線??程數(shù),線程池創(chuàng)建后好后就準(zhǔn)備就緒的線程數(shù),就等待異步任務(wù)去執(zhí)行,new 好了 Thread,等待異步任務(wù)maximumPoolSize
池中最大線程數(shù)量,控制資源并發(fā)keepAliveTime
存活時(shí)間,當(dāng)前正在運(yùn)行的線程數(shù)量,大于核心線程數(shù),就會釋放空閑的線程,只要線程空閑大于指定存活時(shí)間,釋放的線程是指最大的線程數(shù)量減去核心線程數(shù),unit
時(shí)間單位BlockingQueue workQueue
阻塞隊(duì)列,如果任務(wù)有很多,就會將目前多的隊(duì)伍放在隊(duì)列里面,只要有空閑的線程,就會去隊(duì)列里面取出新的任務(wù)繼續(xù)執(zhí)行。new LinkedBlockingQueue<>()
默認(rèn)值是Integer的最大值,會導(dǎo)致內(nèi)存不夠,一定要傳入業(yè)務(wù)定制的大小,可以通過壓測得出峰值threadFactory
線程的創(chuàng)建工廠handler
如果隊(duì)列滿了,按照我們指定的拒絕策略拒絕執(zhí)行任務(wù)
3.線程池的運(yùn)行流程 線程池創(chuàng)建
準(zhǔn)備好core 數(shù)量的核心線程,準(zhǔn)備接受任務(wù)新的任務(wù)進(jìn)來,用core 準(zhǔn)備好的空閑線程執(zhí)行。
(1) 、core 滿了,就將再進(jìn)來的任務(wù)放入阻塞隊(duì)列中??臻e的core 就會自己去阻塞隊(duì)列獲取任務(wù)執(zhí)行
(2) 、阻塞隊(duì)列滿了,就直接開新線程執(zhí)行,最大只能開到max 指定的數(shù)量
(3) 、max 都執(zhí)行好了。Max-core 數(shù)量空閑的線程會在keepAliveTime 指定的時(shí)間后自動銷毀。最終保持到core 大小
(4) 、如果線程數(shù)開到了max 的數(shù)量,還有新任務(wù)進(jìn)來,就會使用reject 指定的拒絕策略進(jìn)行處理所有的線程創(chuàng)建都是由指定的factory 創(chuàng)建的。
一個(gè)線程池core 7; max 20 ,queue:50,100 并發(fā)進(jìn)來怎么分配的;先有7 個(gè)能直接得到執(zhí)行,接下來50 個(gè)進(jìn)入隊(duì)列排隊(duì),在多開13 個(gè)繼續(xù)執(zhí)行。
現(xiàn)在70 個(gè)被安排上了。剩下30 個(gè)默認(rèn)拒絕策略。拒絕策略一般是拋棄,如果不想拋棄還要執(zhí)行,可以使用同步的方式執(zhí)行,或者丟棄最老的
4. 四種常見的線程池
newCachedThreadPool
創(chuàng)建一個(gè)可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。核心線程固定是0,所有都可回收newFixedThreadPool
創(chuàng)建一個(gè)固定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊(duì)列中等待。固定大小,核心 = 最大newScheduledThreadPool
創(chuàng)建一個(gè)固定長線程池,支持定時(shí)及周期性任務(wù)執(zhí)行。定時(shí)任務(wù)線程池newSingleThreadExecutor
創(chuàng)建一個(gè)單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。后臺從隊(duì)列里面獲取任務(wù) 挨個(gè)執(zhí)行
為什么要使用線程池
降低資源的消耗
通過重復(fù)利用已經(jīng)創(chuàng)建好的線程降低線程的創(chuàng)建和銷毀帶來的損耗
提高響應(yīng)速度
因?yàn)榫€程池中的線程數(shù)沒有超過線程池的最大上限時(shí),有的線程處于等待分配任務(wù)的狀態(tài),當(dāng)任務(wù)來時(shí)無需創(chuàng)建新的線程就能執(zhí)行
提高線程的可管理性
線程池會根據(jù)當(dāng)前系統(tǒng)特點(diǎn)對池內(nèi)的線程進(jìn)行優(yōu)化處理,減少創(chuàng)建和銷毀線程帶來的系統(tǒng)開銷。無限的創(chuàng)建和銷毀線程不僅消耗系統(tǒng)資源,還降低系統(tǒng)的穩(wěn)定性,使用線程池進(jìn)行統(tǒng)一分配
CompletableFuture 異步編排
業(yè)務(wù)場景:
查詢商品詳情頁的邏輯比較復(fù)雜,有些數(shù)據(jù)還需要遠(yuǎn)程調(diào)用,必然需要花費(fèi)更多的時(shí)間
假如商品詳情頁的每個(gè)查詢,需要如下標(biāo)注的時(shí)間才能完成那么,用戶需要5.5s 后才能看到商品詳情頁的內(nèi)容。很顯然是不能接受的。
如果有多個(gè)線程同時(shí)完成這6 步操作,也許只需要1.5s 即可完成響應(yīng)。
CompletableFuture 和FutureTask 同屬于Future 接口的實(shí)現(xiàn)類,都可以獲取線程的執(zhí)行結(jié)果。
1.創(chuàng)建異步對象
1、runXxxx 都是沒有返回結(jié)果的,supplyXxx 都是可以獲取返回結(jié)果的
2、可以傳入自定義的線程池,否則就用默認(rèn)的線程池;
沒有返回結(jié)果的
static ExecutorService service = Executors.newFixedThreadPool(10); CompletableFuture.runAsync(()->{ System.out.println("當(dāng)前線程:"+Thread.currentThread().getId()); int i = 10 / 2; System.out.println("運(yùn)行結(jié)果:"+i); },service);
有返回結(jié)果的
static ExecutorService service = Executors.newFixedThreadPool(10); CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 2; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service); Integer integer = future.get(); System.out.println("main----end"+integer);
2.計(jì)算完成時(shí)(線程執(zhí)行成功)回調(diào)方法
whenComplete 可以處理正常和異常的計(jì)算結(jié)果,雖然可以得到異常信息,但是不能修改返回?cái)?shù)據(jù)exceptionally 處理異常情況。
可以感知異常并返回默認(rèn)值whenComplete 和whenCompleteAsync 的區(qū)別:
whenComplete
:是執(zhí)行當(dāng)前任務(wù)的線程執(zhí)行繼續(xù)執(zhí)行whenComplete 的任務(wù)。whenCompleteAsync
:是執(zhí)行把whenCompleteAsync 這個(gè)任務(wù)繼續(xù)提交給線程池來進(jìn)行執(zhí)行。
方法不以Async 結(jié)尾,意味著Action 使用相同的線程執(zhí)行,而Async 可能會使用其他線程執(zhí)行(如果是使用相同的線程池,也可能會被同一個(gè)線程選中執(zhí)行)
public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println("main----start"); CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 0; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service).whenComplete((res,excption)->{ System.out.println("異步任務(wù)成功完成:結(jié)果是::::"+res+"異常是:"+excption); }).exceptionally(throwable->{ //可以感知異常,同時(shí)返回?cái)?shù)據(jù) return 10; }); Integer integer = future.get(); System.out.println("main----end"+integer); }
3.handle 方法(可對結(jié)果做最后的處理(可處理異常),可改變返回值)
和complete 一樣,可對結(jié)果做最后的處理(可處理異常),可改變返回值。
/* 方法完成后的處理*/ CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service).handle((res,exption)->{ if (res != null){ return res*2; } if (exption != null){ return 0; } return 0; });
4.線程串行化
thenApply
方法:當(dāng)一個(gè)線程依賴另一個(gè)線程時(shí),獲取上一個(gè)任務(wù)返回的結(jié)果,并返回當(dāng)前任務(wù)的返回值。thenAccept
方法:能接收上一步的返回結(jié)果,但是不能改變返回值。thenRun
方法:只要上面的任務(wù)執(zhí)行完成,就開始執(zhí)行thenRun,不能改變返回值帶有Async 默認(rèn)是異步執(zhí)行的。同之前。
以上都要前置任務(wù)成功完成。
Function<? super T,? extends U>
T
:上一個(gè)任務(wù)返回結(jié)果的類型U
:當(dāng)前任務(wù)的返回值類型thenRun
方法:只要上面的任務(wù)執(zhí)行完成,就開始執(zhí)行thenRun,不能改變返回值
static ExecutorService service = Executors.newFixedThreadPool(10); CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service).thenRunAsync(() -> { System.out.println("任務(wù)2啟動了"); }, service);
thenAccept
方法:能接收上一步的返回結(jié)果,但是不能改變返回值。
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service).thenAccept((res)->{ System.out.println("異步啟動了:"+res); });
thenApplyAsync
技能接收上一步的結(jié)果,又能改變返回值
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("當(dāng)前線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("運(yùn)行結(jié)果:" + i); return i; }, service); future.thenApplyAsync((res) -> { System.out.println("任務(wù)2啟動了:" + res); return res + "hello"; }, service); System.out.println("main----end"+future.get());
5.兩任務(wù)組合- 都要完成
兩個(gè)任務(wù)必須都完成,觸發(fā)該任務(wù)。
thenCombine
:組合兩個(gè)future,獲取兩個(gè)future 的返回結(jié)果,并返回當(dāng)前任務(wù)的返回值thenAcceptBoth
:組合兩個(gè)future,獲取兩個(gè)future 任務(wù)的返回結(jié)果,然后處理任務(wù),沒有返回值。
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)1線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任務(wù)1運(yùn)行結(jié)果:" + i); return i; }, service); CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)2線程:" + Thread.currentThread().getId()); System.out.println("任務(wù)2運(yùn)行結(jié)果:"); return "hello"; }, service); future01.thenAcceptBothAsync(future02, (f1, f2) -> { System.out.println("任務(wù)3開始之前的結(jié)果---f1=" + f1 + "f2=" + f2); }, service);
runAfterBoth
:組合兩個(gè)future,不獲取前兩個(gè)的結(jié)果,只需兩個(gè)future 處理完任務(wù)后,處理該任務(wù)。
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)1線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任務(wù)1運(yùn)行結(jié)果:" + i); return i; }, service); CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)2線程:" + Thread.currentThread().getId()); System.out.println("任務(wù)2運(yùn)行結(jié)果:"); return "hello"; }, service); future01.runAfterBothAsync(future02,()->{ System.out.println("任務(wù)3開始"); },service);
6.兩任務(wù)組合- 一個(gè)完成
當(dāng)兩個(gè)任務(wù)中,任意一個(gè)future 任務(wù)完成的時(shí)候,執(zhí)行任務(wù)。
applyToEither
:兩個(gè)任務(wù)有一個(gè)執(zhí)行完成,獲取它的返回值,處理任務(wù)并自己有新的返回值。
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)1線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任務(wù)1運(yùn)行結(jié)果:" + i); return i; }, service); CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)2線程:" + Thread.currentThread().getId()); System.out.println("任務(wù)2運(yùn)行結(jié)果:"); return "hello"; }, service); future01.applyToEitherAsync(future02,(t) -> { System.out.println("任務(wù)3開始"+t); return t.toString() + "niubi"; }, service);
acceptEither
:兩個(gè)任務(wù)有一個(gè)執(zhí)行完成,獲取它的返回值,處理任務(wù),自己沒有新的返回值。
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)1線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任務(wù)1運(yùn)行結(jié)果:" + i); return i; }, service); CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)2線程:" + Thread.currentThread().getId()); System.out.println("任務(wù)2運(yùn)行結(jié)果:"); return "hello"; }, service); future01.acceptEitherAsync(future02,(t) -> { System.out.println("任務(wù)3開始"+t); }, service);
runAfterEither
:兩個(gè)任務(wù)有一個(gè)執(zhí)行完成,不獲取future 的結(jié)果,處理任務(wù),自己也沒有返回值。
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)1線程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任務(wù)1運(yùn)行結(jié)果:" + i); return i; }, service); CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任務(wù)2線程:" + Thread.currentThread().getId()); System.out.println("任務(wù)2運(yùn)行結(jié)果:"); return "hello"; }, service); future01.runAfterEitherAsync(future02,() -> { System.out.println("任務(wù)3開始"); }, service);
7.多任務(wù)組合
allOf
:等待所有任務(wù)完成
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> { System.out.println("查詢商品的圖片信息"); return "hello.png"; }, service); CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> { System.out.println("查詢商品的屬性"); return "黑色+256g"; }, service); CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> { System.out.println("查詢商品的介紹"); return "華為"; }, service); CompletableFuture<Void> completableFuture = CompletableFuture.allOf(futureImg, futureAttr, futureDesc); completableFuture.get(); //等待所有結(jié)果完成
anyOf
:只要有一個(gè)任務(wù)完成
整合SpringBoot
1.添加配置類,新建線程池
package cn.cloud.xmall.product.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @Description: ··· * @author: Freedom * @QQ: 1556507698 * @date:2022/3/21 17:41 */ @Configuration public class MyThreadConfig { @Bean public ThreadPoolExecutor threadPoolExecutor(){ return new ThreadPoolExecutor( 20, 200, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); }; }
2.想要在配置文件中手動的配置參數(shù)
新建一個(gè)配置屬性類
package cn.cloud.xmall.product.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; /** * @Description: ··· * @author: Freedom * @QQ: 1556507698 * @date:2022/3/21 17:47 */ @ConfigurationProperties(prefix = "xmall.thread") @Component //加入容器 @Data public class ThreadPollConfigProperties { private Integer coreSize; private Integer maxSize; private Integer keepAliveTime; }
注:可以在依賴種添加此依賴,在配置文件中就會有我們自己配置屬性的提示
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
3.配置文件配置屬性
#線程池配置 xmall: thread: core-size: 20 max-size: 200 keep-alive-time: 10
4.使用配置文件中的屬性
@EnableConfigurationProperties(ThreadPollConfigProperties.class),如果配置文件類沒有添加@Component加入容器可以使用這種方式
package cn.cloud.xmall.product.config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @Description: ··· * @author: Freedom * @QQ: 1556507698 * @date:2022/3/21 17:41 */ //@EnableConfigurationProperties(ThreadPollConfigProperties.class) @Configuration public class MyThreadConfig { @Bean public ThreadPoolExecutor threadPoolExecutor(ThreadPollConfigProperties pool){ return new ThreadPoolExecutor( pool.getCoreSize(), pool.getMaxSize(), pool.getKeepAliveTime(), TimeUnit.SECONDS, new LinkedBlockingQueue<>(100000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); }; }
5.注入線程池
@Autowired private ThreadPoolExecutor executor;
6.異步編排
@Override public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException { SkuItemVo skuItemVo = new SkuItemVo(); //1.使用自己的線程池來新建異步任務(wù) CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> { //1.查詢基本信息 pms_sku_info SkuInfoEntity info = getById(skuId); skuItemVo.setInfo(info); return info; }, executor); //2.根據(jù)一號任務(wù)來繼續(xù)調(diào)用 CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> { //3.獲取當(dāng)前spu的銷售屬性組合 List<SkuItemSaleAttrVo> saleAttrVos = saleAttrValueService.getSaleAttrsBySpuId(res.getSpuId()); skuItemVo.setSaleAttr(saleAttrVos); }, executor); //3.根據(jù)一號任務(wù)來繼續(xù)調(diào)用 CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> { //4.獲取Spu的介紹 pms_spu_info_desc SpuInfoDescEntity spuInfo = spuInfoDescService.getById(res.getSpuId()); skuItemVo.setDesc(spuInfo); }, executor); //4.根據(jù)一號任務(wù)來繼續(xù)調(diào)用 CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> { //5.獲取spu的規(guī)格參數(shù)信息 List<SpuItemAttrGroupVo> attrGroups = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId()); skuItemVo.setGroupAttrs(attrGroups); }, executor); //此任務(wù)不需要根據(jù)一號任務(wù)的返回調(diào)用,所以開一個(gè)新線程 CompletableFuture<Void> imagesFuture = CompletableFuture.runAsync(() -> { //2.獲取sku的圖片信息 pms_sku_images List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId); skuItemVo.setImages(images); }, executor); //等待所有任務(wù)都完成 //TODO 可以選擇有異常情況下的處理結(jié)果 CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imagesFuture).get(); return skuItemVo; }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java中的HashSet詳解和使用示例_動力節(jié)點(diǎn)Java學(xué)院整理
HashSet 是一個(gè)沒有重復(fù)元素的集合。接下來通過實(shí)例代碼給大家介紹java中的hashset相關(guān)知識,感興趣的朋友一起看看吧2017-05-05java數(shù)據(jù)結(jié)構(gòu)ArrayList詳解
本文詳細(xì)講解了java數(shù)據(jù)結(jié)構(gòu)ArrayList的用法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解
這篇文章主要介紹了SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Java面試題沖刺第六天--網(wǎng)絡(luò)編程1
這篇文章主要為大家分享了最有價(jià)值的三道網(wǎng)絡(luò)編程面試題,涵蓋內(nèi)容全面,包括數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的題目、經(jīng)典面試編程題等,感興趣的小伙伴們可以參考一下2021-07-07Java使用selenium爬取b站動態(tài)的實(shí)現(xiàn)方式
本文主要介紹了Java使用selenium爬取b站動態(tài)的實(shí)現(xiàn)方式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01Spring的兩種事務(wù)管理機(jī)制的基本概念和demo示例
Spring事務(wù)包括聲明式事務(wù)管理和注解式事務(wù)管理,我們通過概念和小demo的形式一步一步地來一起學(xué)習(xí)這個(gè)知識點(diǎn),需要的朋友可以參考下2023-07-07JAVA中ListIterator和Iterator詳解與辨析(推薦)
這篇文章主要介紹了JAVA中ListIterator和Iterator詳解與辨析,需要的朋友可以參考下2017-04-04