面试-线程池的成长之路
轉(zhuǎn)載自? ?面試-線程池的成長(zhǎng)之路
背景
相信大家在面試過(guò)程中遇到面試官問(wèn)線程的很多,線程過(guò)后就是線程池了。從易到難,都是這么個(gè)過(guò)程,還有就是確實(shí)很多人在工作中接觸線程池比較少,最多的也就是創(chuàng)建一個(gè)然后往里面提交線程,對(duì)于一些經(jīng)驗(yàn)很豐富的面試官來(lái)說(shuō),一下就可以問(wèn)出很多線程池相關(guān)的問(wèn)題,與其被問(wèn)的暈頭轉(zhuǎn)向,還不如好好學(xué)習(xí)。此時(shí)不努力更待何時(shí)。
什么是線程池?
線程池是一種多線程處理形式,處理過(guò)程中將任務(wù)提交到線程池,任務(wù)的執(zhí)行交由線程池來(lái)管理。
如果每個(gè)請(qǐng)求都創(chuàng)建一個(gè)線程去處理,那么服務(wù)器的資源很快就會(huì)被耗盡,使用線程池可以減少創(chuàng)建和銷毀線程的次數(shù),每個(gè)工作線程都可以被重復(fù)利用,可執(zhí)行多個(gè)任務(wù)。
如果用生活中的列子來(lái)說(shuō)明,我們可以把線程池當(dāng)做一個(gè)客服團(tuán)隊(duì),如果同時(shí)有1000個(gè)人打電話進(jìn)行咨詢,按照正常的邏輯那就是需要1000個(gè)客服接聽(tīng)電話,服務(wù)客戶。現(xiàn)實(shí)往往需要考慮到很多層面的東西,比如:資源夠不夠,招這么多人需要費(fèi)用比較多。正常的做法就是招100個(gè)人成立一個(gè)客服中心,當(dāng)有電話進(jìn)來(lái)后分配沒(méi)有接聽(tīng)的客服進(jìn)行服務(wù),如果超出了100個(gè)人同時(shí)咨詢的話,提示客戶等待,稍后處理,等有客服空出來(lái)就可以繼續(xù)服務(wù)下一個(gè)客戶,這樣才能達(dá)到一個(gè)資源的合理利用,實(shí)現(xiàn)效益的最大化。
Java中的線程池種類
1. newSingleThreadExecutor
創(chuàng)建方式:
ExecutorService pool = Executors.newSingleThreadExecutor();一個(gè)單線程的線程池。這個(gè)線程池只有一個(gè)線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)。如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束,那么會(huì)有一個(gè)新的線程來(lái)替代它。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。
使用方式:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPool {public static void main(String[] args) {ExecutorService pool = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {pool.execute(() -> {System.out.println(Thread.currentThread().getName() + "\t開(kāi)始發(fā)車?yán)?...");});}} }輸出結(jié)果如下:
pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?...從輸出的結(jié)果我們可以看出,一直只有一個(gè)線程在運(yùn)行。
2.newFixedThreadPool
創(chuàng)建方式:
ExecutorService pool = Executors.newFixedThreadPool(10);創(chuàng)建固定大小的線程池。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大小。線程池的大小一旦達(dá)到最大值就會(huì)保持不變,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束,那么線程池會(huì)補(bǔ)充一個(gè)新線程。
使用方式:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPool {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {pool.execute(() -> {System.out.println(Thread.currentThread().getName() + "\t開(kāi)始發(fā)車?yán)?...");});}} }輸出結(jié)果如下:
pool-1-thread-1 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-4 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-3 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-2 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-6 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-7 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-5 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-8 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-9 ? ?開(kāi)始發(fā)車?yán)?... pool-1-thread-10 開(kāi)始發(fā)車?yán)?...3. newCachedThreadPool
創(chuàng)建方式:
ExecutorService pool = Executors.newCachedThreadPool();創(chuàng)建一個(gè)可緩存的線程池。如果線程池的大小超過(guò)了處理任務(wù)所需要的線程,那么就會(huì)回收部分空閑的線程,當(dāng)任務(wù)數(shù)增加時(shí),此線程池又添加新線程來(lái)處理任務(wù)。
使用方式如上2所示。
4.newScheduledThreadPool
創(chuàng)建方式:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
使用方式:
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPool {public static void main(String[] args) {ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);for (int i = 0; i < 10; i++) {pool.schedule(() -> {System.out.println(Thread.currentThread().getName() + "\t開(kāi)始發(fā)車?yán)?...");}, 10, TimeUnit.SECONDS);}} }上面演示的是延遲10秒執(zhí)行任務(wù),如果想要執(zhí)行周期性的任務(wù)可以用下面的方式,每秒執(zhí)行一次
//pool.scheduleWithFixedDelay也可以 pool.scheduleAtFixedRate(() -> {System.out.println(Thread.currentThread().getName() + "\t開(kāi)始發(fā)車?yán)?..."); }, 1, 1, TimeUnit.SECONDS);5.newWorkStealingPool
newWorkStealingPool是jdk1.8才有的,會(huì)根據(jù)所需的并行層次來(lái)動(dòng)態(tài)創(chuàng)建和關(guān)閉線程,通過(guò)使用多個(gè)隊(duì)列減少競(jìng)爭(zhēng),底層用的ForkJoinPool來(lái)實(shí)現(xiàn)的。ForkJoinPool的優(yōu)勢(shì)在于,可以充分利用多cpu,多核cpu的優(yōu)勢(shì),把一個(gè)任務(wù)拆分成多個(gè)“小任務(wù)”,把多個(gè)“小任務(wù)”放到多個(gè)處理器核心上并行執(zhí)行;當(dāng)多個(gè)“小任務(wù)”執(zhí)行完成之后,再將這些執(zhí)行結(jié)果合并起來(lái)即可。
說(shuō)說(shuō)線程池的拒絕策略
當(dāng)請(qǐng)求任務(wù)不斷的過(guò)來(lái),而系統(tǒng)此時(shí)又處理不過(guò)來(lái)的時(shí)候,我們需要采取的策略是拒絕服務(wù)。RejectedExecutionHandler接口提供了拒絕任務(wù)處理的自定義方法的機(jī)會(huì)。在ThreadPoolExecutor中已經(jīng)包含四種處理策略。
-
AbortPolicy策略:該策略會(huì)直接拋出異常,阻止系統(tǒng)正常工作。
- CallerRunsPolicy 策略:只要線程池未關(guān)閉,該策略直接在調(diào)用者線程中,運(yùn)行當(dāng)前的被丟棄的任務(wù)。
- DiscardOleddestPolicy策略: 該策略將丟棄最老的一個(gè)請(qǐng)求,也就是即將被執(zhí)行的任務(wù),并嘗試再次提交當(dāng)前任務(wù)。
- DiscardPolicy策略:該策略默默的丟棄無(wú)法處理的任務(wù),不予任何處理。
除了JDK默認(rèn)為什么提供的四種拒絕策略,我們可以根據(jù)自己的業(yè)務(wù)需求去自定義拒絕策略,自定義的方式很簡(jiǎn)單,直接實(shí)現(xiàn)RejectedExecutionHandler接口即可
比如Spring integration中就有一個(gè)自定義的拒絕策略CallerBlocksPolicy,將任務(wù)插入到隊(duì)列中,直到隊(duì)列中有空閑并插入成功的時(shí)候,否則將根據(jù)最大等待時(shí)間一直阻塞,直到超時(shí)。
package org.springframework.integration.util; import java.util.concurrent.BlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class CallerBlocksPolicy implements RejectedExecutionHandler {private static final Log logger = LogFactory.getLog(CallerBlocksPolicy.class);private final long maxWait;/*** @param maxWait The maximum time to wait for a queue slot to be* available, in milliseconds.*/public CallerBlocksPolicy(long maxWait) {this.maxWait = maxWait;}@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {if (!executor.isShutdown()) {try {BlockingQueue<Runnable> queue = executor.getQueue();if (logger.isDebugEnabled()) {logger.debug("Attempting to queue task execution for " + this.maxWait + " milliseconds");}if (!queue.offer(r, this.maxWait, TimeUnit.MILLISECONDS)) {throw new RejectedExecutionException("Max wait time expired to queue task");}if (logger.isDebugEnabled()) {logger.debug("Task execution queued");}}catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RejectedExecutionException("Interrupted", e);}}else {throw new RejectedExecutionException("Executor has been shut down");}} }定義好之后如何使用呢?光定義沒(méi)用的呀,一定要用到線程池中呀,可以通過(guò)下面的方式自定義線程池,指定拒絕策略。
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 100, 10, TimeUnit.SECONDS, workQueue, new CallerBlocksPolicy());execute和submit的區(qū)別?
在前面的講解中,我們執(zhí)行任務(wù)是用的execute方法,除了execute方法,還有一個(gè)submit方法也可以執(zhí)行我們提交的任務(wù)。
這兩個(gè)方法有什么區(qū)別呢?分別適用于在什么場(chǎng)景下呢?我們來(lái)做一個(gè)簡(jiǎn)單的分析。
execute適用于不需要關(guān)注返回值的場(chǎng)景,只需要將線程丟到線程池中去執(zhí)行就可以了
public class ThreadPool {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);pool.execute(() -> {System.out.println(Thread.currentThread().getName() + "\t開(kāi)始發(fā)車?yán)?...");});} }submit方法適用于需要關(guān)注返回值的場(chǎng)景,submit方法的定義如下:
public interface ExecutorService extends Executor {...<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);Future<?> submit(Runnable task);... }其子類AbstractExecutorService實(shí)現(xiàn)了submit方法,可以看到無(wú)論參數(shù)是Callable還是Runnable,最終都會(huì)被封裝成RunnableFuture,然后再調(diào)用execute執(zhí)行。
? ?/*** @throws RejectedExecutionException {@inheritDoc}* @throws NullPointerException ? ? ? {@inheritDoc}*/public Future<?> submit(Runnable task) {if (task == null) throw new NullPointerException();RunnableFuture<Void> ftask = newTaskFor(task, null);execute(ftask);return ftask;}/*** @throws RejectedExecutionException {@inheritDoc}* @throws NullPointerException ? ? ? {@inheritDoc}*/public <T> Future<T> submit(Runnable task, T result) {if (task == null) throw new NullPointerException();RunnableFuture<T> ftask = newTaskFor(task, result);execute(ftask);return ftask;}/*** @throws RejectedExecutionException {@inheritDoc}* @throws NullPointerException ? ? ? {@inheritDoc}*/public <T> Future<T> submit(Callable<T> task) {if (task == null) throw new NullPointerException();RunnableFuture<T> ftask = newTaskFor(task);execute(ftask);return ftask;}下面我們來(lái)看看這三個(gè)方法分別如何去使用:
submit(Callable?task);
public class ThreadPool {public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(10);Future<String> future = pool.submit(new Callable<String>() {@Overridepublic String call() throws Exception {return "Hello";}});String result = future.get();System.out.println(result);} }submit(Runnable task, T result);
public class ThreadPool {public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(10);Data data = new Data();Future<Data> future = pool.submit(new MyRunnable(data), data);String result = future.get().getName();System.out.println(result);} } class Data {String name;public String getName() {return name;}public void setName(String name) {this.name = name;} } class MyRunnable implements Runnable {private Data data;public MyRunnable(Data data) {this.data = data;}@Overridepublic void run() {data.setName("yinjihuan");} }Future?submit(Runnable task);
直接submit一個(gè)Runnable是拿不到返回值的,返回值就是null.
五種線程池的使用場(chǎng)景
- newSingleThreadExecutor:一個(gè)單線程的線程池,可以用于需要保證順序執(zhí)行的場(chǎng)景,并且只有一個(gè)線程在執(zhí)行。
- newFixedThreadPool:一個(gè)固定大小的線程池,可以用于已知并發(fā)壓力的情況下,對(duì)線程數(shù)做限制。
- newCachedThreadPool:一個(gè)可以無(wú)限擴(kuò)大的線程池,比較適合處理執(zhí)行時(shí)間比較小的任務(wù)。
- newScheduledThreadPool:可以延時(shí)啟動(dòng),定時(shí)啟動(dòng)的線程池,適用于需要多個(gè)后臺(tái)線程執(zhí)行周期任務(wù)的場(chǎng)景。
- newWorkStealingPool:一個(gè)擁有多個(gè)任務(wù)隊(duì)列的線程池,可以減少連接數(shù),創(chuàng)建當(dāng)前可用cpu數(shù)量的線程來(lái)并行執(zhí)行。
線程池的關(guān)閉
關(guān)閉線程池可以調(diào)用shutdownNow和shutdown兩個(gè)方法來(lái)實(shí)現(xiàn)
shutdownNow:對(duì)正在執(zhí)行的任務(wù)全部發(fā)出interrupt(),停止執(zhí)行,對(duì)還未開(kāi)始執(zhí)行的任務(wù)全部取消,并且返回還沒(méi)開(kāi)始的任務(wù)列表
public class ThreadPool {public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(1);for (int i = 0; i < 5; i++) {System.err.println(i);pool.execute(() -> {try {Thread.sleep(30000);System.out.println("--");} catch (Exception e) {e.printStackTrace();}});}Thread.sleep(1000);List<Runnable> runs = pool.shutdownNow();} }上面的代碼模擬了立即取消的場(chǎng)景,往線程池里添加5個(gè)線程任務(wù),然后sleep一段時(shí)間,線程池只有一個(gè)線程,如果此時(shí)調(diào)用shutdownNow后應(yīng)該需要中斷一個(gè)正在執(zhí)行的任務(wù)和返回4個(gè)還未執(zhí)行的任務(wù),控制臺(tái)輸出下面的內(nèi)容:
[fs.ThreadPool$$Lambda$1/990368553@682a0b20, fs.ThreadPool$$Lambda$1/990368553@682a0b20, fs.ThreadPool$$Lambda$1/990368553@682a0b20, fs.ThreadPool$$Lambda$1/990368553@682a0b20] java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at fs.ThreadPool.lambda$0(ThreadPool.java:15)at fs.ThreadPool$$Lambda$1/990368553.run(Unknown Source)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)shutdown:當(dāng)我們調(diào)用shutdown后,線程池將不再接受新的任務(wù),但也不會(huì)去強(qiáng)制終止已經(jīng)提交或者正在執(zhí)行中的任務(wù)
public class ThreadPool {public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(1);for (int i = 0; i < 5; i++) {System.err.println(i);pool.execute(() -> {try {Thread.sleep(30000);System.out.println("--");} catch (Exception e) {e.printStackTrace();}});}Thread.sleep(1000);pool.shutdown();pool.execute(() -> {try {Thread.sleep(30000);System.out.println("--");} catch (Exception e) {e.printStackTrace();}});} }上面的代碼模擬了正在運(yùn)行的狀態(tài),然后調(diào)用shutdown,接著再往里面添加任務(wù),肯定是拒絕添加的,請(qǐng)看輸出結(jié)果:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task fs.ThreadPool$$Lambda$2/1747585824@3d075dc0 rejected from java.util.concurrent.ThreadPoolExecutor@214c265e[Shutting down, pool size = 1, active threads = 1, queued tasks = 4, completed tasks = 0]at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)at fs.ThreadPool.main(ThreadPool.java:24)還有一些業(yè)務(wù)場(chǎng)景下需要知道線程池中的任務(wù)是否全部執(zhí)行完成,當(dāng)我們關(guān)閉線程池之后,可以用isTerminated來(lái)判斷所有的線程是否執(zhí)行完成,千萬(wàn)不要用isShutdown,isShutdown只是返回你是否調(diào)用過(guò)shutdown的結(jié)果。
public class ThreadPool {public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(1);for (int i = 0; i < 5; i++) {System.err.println(i);pool.execute(() -> {try {Thread.sleep(3000);System.out.println("--");} catch (Exception e) {e.printStackTrace();}});}Thread.sleep(1000);pool.shutdown();while(true){ ?if(pool.isTerminated()){ ?System.out.println("所有的子線程都結(jié)束了!"); ?break; ?} ?Thread.sleep(1000); ? ?} ?} }自定義線程池
在實(shí)際的使用過(guò)程中,大部分我們都是用Executors去創(chuàng)建線程池直接使用,如果有一些其他的需求,比如指定線程池的拒絕策略,阻塞隊(duì)列的類型,線程名稱的前綴等等,我們可以采用自定義線程池的方式來(lái)解決。
如果只是簡(jiǎn)單的想要改變線程名稱的前綴的話可以自定義ThreadFactory來(lái)實(shí)現(xiàn),在Executors.new…中有一個(gè)ThreadFactory的參數(shù),如果沒(méi)有指定則用的是DefaultThreadFactory。
自定義線程池核心在于創(chuàng)建一個(gè)ThreadPoolExecutor對(duì)象,指定參數(shù)
下面我們看下ThreadPoolExecutor構(gòu)造函數(shù)的定義:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) ;- corePoolSize
線程池大小,決定著新提交的任務(wù)是新開(kāi)線程去執(zhí)行還是放到任務(wù)隊(duì)列中,也是線程池的最最核心的參數(shù)。一般線程池開(kāi)始時(shí)是沒(méi)有線程的,只有當(dāng)任務(wù)來(lái)了并且線程數(shù)量小于corePoolSize才會(huì)創(chuàng)建線程。
- maximumPoolSize
最大線程數(shù),線程池能創(chuàng)建的最大線程數(shù)量。
- keepAliveTime
在線程數(shù)量超過(guò)corePoolSize后,多余空閑線程的最大存活時(shí)間。
- unit
時(shí)間單位
- workQueue
存放來(lái)不及處理的任務(wù)的隊(duì)列,是一個(gè)BlockingQueue。
- threadFactory
生產(chǎn)線程的工廠類,可以定義線程名,優(yōu)先級(jí)等。
- handler
拒絕策略,當(dāng)任務(wù)來(lái)不及處理的時(shí)候,如何處理, 前面有講解。
了解上面的參數(shù)信息后我們就可以定義自己的線程池了,我這邊用ArrayBlockingQueue替換了LinkedBlockingQueue,指定了隊(duì)列的大小,當(dāng)任務(wù)超出隊(duì)列大小之后使用CallerRunsPolicy拒絕策略處理。
這樣做的好處是嚴(yán)格控制了隊(duì)列的大小,不會(huì)出現(xiàn)一直往里面添加任務(wù)的情況,有的時(shí)候任務(wù)處理的比較慢,任務(wù)數(shù)量過(guò)多會(huì)占用大量?jī)?nèi)存,導(dǎo)致內(nèi)存溢出。
當(dāng)然你也可以在提交到線程池的入口進(jìn)行控制,比如用CountDownLatch, Semaphore等。
/*** 自定義線程池<br>* 默認(rèn)的newFixedThreadPool里的LinkedBlockingQueue是一個(gè)無(wú)邊界隊(duì)列,如果不斷的往里加任務(wù),最終會(huì)導(dǎo)致內(nèi)存的不可控<br>* 增加了有邊界的隊(duì)列,使用了CallerRunsPolicy拒絕策略* @author yinjihuan**/ public class FangjiaThreadPoolExecutor {private static ExecutorService executorService = newFixedThreadPool(50);private static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(10000), new DefaultThreadFactory(), new CallerRunsPolicy());}public static void execute(Runnable command) {executorService.execute(command);}public static void shutdown() {executorService.shutdown();}static class DefaultThreadFactory implements ThreadFactory {private static final AtomicInteger poolNumber = new AtomicInteger(1);private final ThreadGroup group;private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();namePrefix = "FSH-pool-" +poolNumber.getAndIncrement() +"-thread-";}public Thread newThread(Runnable r) {Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon())t.setDaemon(false);if (t.getPriority() != Thread.NORM_PRIORITY)t.setPriority(Thread.NORM_PRIORITY);return t;}} }?
總結(jié)
以上是生活随笔為你收集整理的面试-线程池的成长之路的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 马斯克:X(推特)将新增两种订阅服务,价
- 下一篇: 适马 10-18mm F2.8 DC D