摘要:介紹線(xiàn)程池一般包含三個(gè)主要部分調(diào)度器決定由哪個(gè)線(xiàn)程來(lái)執(zhí)行任務(wù)執(zhí)行任務(wù)所能夠的最大耗時(shí)等線(xiàn)程隊(duì)列存放并管理著一系列線(xiàn)程這些線(xiàn)程都處于阻塞狀態(tài)或休眠狀態(tài)任務(wù)隊(duì)列存放著用戶(hù)提交的需要被執(zhí)行的任務(wù)一般任務(wù)的執(zhí)行的即先提交的任務(wù)先被執(zhí)行調(diào)度器并非是必
介紹
線(xiàn)程池一般包含三個(gè)主要部分:
調(diào)度器: 決定由哪個(gè)線(xiàn)程來(lái)執(zhí)行任務(wù), 執(zhí)行任務(wù)所能夠的最大耗時(shí)等
線(xiàn)程隊(duì)列: 存放并管理著一系列線(xiàn)程, 這些線(xiàn)程都處于阻塞狀態(tài)或休眠狀態(tài)
任務(wù)隊(duì)列: 存放著用戶(hù)提交的需要被執(zhí)行的任務(wù). 一般任務(wù)的執(zhí)行 FIFO 的, 即先提交的任務(wù)先被執(zhí)行
調(diào)度器并非是必須的, 例如 Java 中實(shí)現(xiàn)的 ThreadPoolExecutor 就沒(méi)有調(diào)度器, 而是所有的線(xiàn)程都不斷從任務(wù)隊(duì)列中取出任務(wù), 然后執(zhí)行.線(xiàn)程池模型可以用下圖簡(jiǎn)單地表示
構(gòu)造函數(shù)public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
一般構(gòu)造函數(shù)包含了最主要的成員變量,我們來(lái)看看幾個(gè)參數(shù)
corePoolSize:線(xiàn)程池的最小線(xiàn)程數(shù)量
maximumPoolSize:線(xiàn)程池的最大線(xiàn)程數(shù)量
workQueue:任務(wù)隊(duì)列
threadFactory:產(chǎn)生線(xiàn)程的工廠(chǎng)
keepAliveTime:允許的最大idle時(shí)間
handler:拒絕執(zhí)行處理器
這些是可控制的線(xiàn)程池的參數(shù)
RUNNING: 接受新任務(wù)并處理隊(duì)列中的任務(wù)
SHUTDOWN: 不接受新任務(wù),但是處理隊(duì)列中的任務(wù)
STOP: 不接受新任務(wù),也不處理隊(duì)列中的任務(wù),還會(huì)中斷已經(jīng)進(jìn)行中的任務(wù)
TIDYING: 所有任務(wù)已經(jīng)執(zhí)行完畢,工作線(xiàn)程數(shù)量為0,線(xiàn)程池狀態(tài)轉(zhuǎn)換為T(mén)IDYING,terminate方法被出觸發(fā)。
TERMINATED: terminated() 執(zhí)行完畢
狀態(tài)轉(zhuǎn)換
RUNNING -> SHUTDOWN:shutdown()
(RUNNING or SHUTDOWN) -> STOP:shutdownNow()
SHUTDOWN -> TIDYING:線(xiàn)程池和任務(wù)隊(duì)列都為空
STOP -> TIDYING:線(xiàn)程池為空
TIDYING -> TERMINATED:terminated()
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
狀態(tài)碼是由一個(gè)32位的原子Int的前三位表示的,后三位表示工作線(xiàn)程的數(shù)量
線(xiàn)程存在哪里?真正的工作線(xiàn)程封裝成一個(gè)內(nèi)部類(lèi)Worker,存放在HashSet中
private final HashSetsubmit()workers = new HashSet (); private final class Worker extends AbstractQueuedSynchronizer implements Runnable { /** 真正的工作線(xiàn)程 */ final Thread thread; /** 要執(zhí)行的任務(wù) */ Runnable firstTask; /** 已經(jīng)完成的任務(wù)計(jì)數(shù)器 */ volatile long completedTasks; Worker(Runnable firstTask) { setState(-1); // 阻止中斷 this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } /** 只是一個(gè)家的代理,真實(shí)的執(zhí)行方法在runWorker里 */ public void run() { runWorker(this); } //下面就是一個(gè)不可重入的排他鎖 protected boolean isHeldExclusively() { return getState() != 0; } protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); } public void unlock() { release(1); } public boolean isLocked() { return isHeldExclusively(); } void interruptIfStarted() { Thread t; if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) { } } } }
ThreadPoolExecutor實(shí)現(xiàn)了ExecutorService接口
// Class:ExecutorService // 提交一個(gè)待執(zhí)行的Runnable任務(wù),并返回FutureFuture submit(Callable task); Future submit(Runnable task, T result); Future> submit(Runnable task);
public Future> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFutureftask = newTaskFor(task, null); execute(ftask); return ftask; } public Future submit(Runnable task, T result) { if (task == null) throw new NullPointerException(); RunnableFuture ftask = newTaskFor(task, result); execute(ftask); return ftask; } public Future submit(Callable task) { if (task == null) throw new NullPointerException(); RunnableFuture ftask = newTaskFor(task); execute(ftask); return ftask; } protected RunnableFuture newTaskFor(Runnable runnable, T value) { return new FutureTask (runnable, value); }
再看ThreadPoolExcutor的實(shí)現(xiàn),submit方法都委托給execute執(zhí)行了。
當(dāng)我們通過(guò) execute(Runnable) 提交一個(gè)任務(wù)時(shí):
如果此時(shí)線(xiàn)程池中線(xiàn)程個(gè)數(shù)小于 corePoolSize, 則此任務(wù)不會(huì)插入到任務(wù)隊(duì)列中, 而是直接創(chuàng)建一個(gè)新的線(xiàn)程來(lái)執(zhí)行此任務(wù), 即使當(dāng)前線(xiàn)程池中有空閑的線(xiàn)程.
如果線(xiàn)程數(shù)大于 corePoolSize 但是小于 maximumPoolSize:
如果任務(wù)隊(duì)列還未滿(mǎn), 則會(huì)將此任務(wù)插入到任務(wù)隊(duì)列末尾;
如果此時(shí)任務(wù)隊(duì)列已滿(mǎn), 則會(huì)創(chuàng)建新的線(xiàn)程來(lái)執(zhí)行此任務(wù).
如果線(xiàn)程數(shù)等于 maximumPoolSize:
如果任務(wù)隊(duì)列還未滿(mǎn), 則會(huì)將此任務(wù)插入到任務(wù)隊(duì)列末尾;
如果此時(shí)任務(wù)隊(duì)列已滿(mǎn), 則會(huì)又 RejectedExecutionHandler 處理, 默認(rèn)情況下是拋出 RejectedExecutionException 異常.
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
上面的代碼有三個(gè)步驟, 首先第一步是檢查當(dāng)前線(xiàn)程池的線(xiàn)程數(shù)是否小于 corePoolSize, 如果小于, 那么由我們前面提到的規(guī)則, 線(xiàn)程池會(huì)創(chuàng)建一個(gè)新的線(xiàn)程來(lái)執(zhí)行此任務(wù), 因此在第一個(gè) if 語(yǔ)句中, 會(huì)調(diào)用 addWorker(command, true) 來(lái)創(chuàng)建一個(gè)新 Worker 線(xiàn)程, 并執(zhí)行此任務(wù). addWorker 的第二個(gè)參數(shù)是一個(gè) boolean 類(lèi)型的, 它的作用是用于標(biāo)識(shí)是否需要使用 corePoolSize 字段, 如果它為真, 則添加新任務(wù)時(shí), 需要考慮到 corePoolSize 字段的影響. 這里至于 addWorker 內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)我們暫且不管, 先把整個(gè)提交任務(wù)的大體脈絡(luò)理清了再說(shuō).
如果前面的判斷不滿(mǎn)足, 那么會(huì)將此任務(wù)插入到工作隊(duì)列中, 即 workQueue.offer(command). 當(dāng)然, 為了健壯性考慮, 當(dāng)插入到 workQueue 后, 我們還需要再次檢查一下此時(shí)線(xiàn)程池是否還是 RUNNING 狀態(tài), 如果不是的話(huà)就會(huì)將原來(lái)插入隊(duì)列中的那個(gè)任務(wù)刪除, 然后調(diào)用 reject 方法拒絕此任務(wù)的提交; 接著考慮到在我們插入任務(wù)到 workQueue 中的同時(shí), 如果此時(shí)線(xiàn)程池中的線(xiàn)程都執(zhí)行完畢并終止了, 在這樣的情況下剛剛插入到 workQueue 中的任務(wù)就永遠(yuǎn)不會(huì)得到執(zhí)行了. 為了避免這樣的情況, 因此我們由再次檢查一下線(xiàn)程池中的線(xiàn)程數(shù), 如果為零, 則調(diào)用 addWorker(null, false) 來(lái)添加一個(gè)線(xiàn)程.
如果前面所分析的情況都不滿(mǎn)足, 那么就會(huì)進(jìn)入到第三個(gè) if 判斷, 在這里會(huì)調(diào)用 addWorker(command, false) 來(lái)將此任務(wù)提交到線(xiàn)程池中. 注意到這個(gè)方法的第二個(gè)參數(shù)是 false, 表示我們?cè)诖舜握{(diào)用 addWorker 時(shí), 不考慮 corePoolSize 的影響, 即忽略 corePoolSize 字段.
前面我們大體分析了一下 execute 提交任務(wù)的流程, 不過(guò)省略了一個(gè)關(guān)鍵步驟, 即 addWorker 方法. 現(xiàn)在我們就來(lái)揭開(kāi)它的神秘面紗吧.
首先看一下 addWorker 方法的簽名:
private boolean addWorker(Runnable firstTask, boolean core)
這個(gè)方法接收兩個(gè)參數(shù), 第一個(gè)是一個(gè) Runnable 類(lèi)型的, 一般來(lái)說(shuō)是我們調(diào)用 execute 方法所傳輸?shù)膮?shù), 不過(guò)也有可能是 null 值, 這樣的情況我們?cè)谇懊嬉恍」?jié)中也見(jiàn)到過(guò).
那么第二個(gè)參數(shù)是做什么的呢? 第二個(gè)參數(shù)是一個(gè) boolean 類(lèi)型的變量, 它的作用是標(biāo)識(shí)是否使用 corePoolSize 屬性. 我們知道, ThreadPoolExecutor 中, 有一個(gè) corePoolSize 屬性, 用于動(dòng)態(tài)調(diào)整線(xiàn)程池中的核心線(xiàn)程數(shù). 那么當(dāng) core 這個(gè)參數(shù)是 true 時(shí), 則表示在添加新任務(wù)時(shí), 需要考慮到 corePoolSzie 的影響(例如如果此時(shí)線(xiàn)程數(shù)已經(jīng)大于 corePoolSize 了, 那么就不能再添加新線(xiàn)程了); 當(dāng) core 為 false 時(shí), 就不考慮 corePoolSize 的影響(其實(shí)代碼中是以 maximumPoolSize 作為 corePoolSize 來(lái)做判斷條件的), 一有新任務(wù), 就對(duì)應(yīng)地生成一個(gè)新的線(xiàn)程.
說(shuō)了這么多, 還不如來(lái)看一下 addWorker 的源碼吧:
private boolean addWorker(Runnable firstTask, boolean core) { // 這里一大段的 for 語(yǔ)句, 其實(shí)就是判斷和處理 core 參數(shù)的. // 當(dāng)經(jīng)過(guò)判斷, 如果當(dāng)前的線(xiàn)程大于 corePoolSize 或 maximumPoolSize 時(shí)(根據(jù) core 的值來(lái)判斷), // 則表示不能新建新的 Worker 線(xiàn)程, 此時(shí)返回 false. retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); // Check if queue empty only if necessary. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { int wc = workerCountOf(c); // 當(dāng) core 為真, 那么就判斷當(dāng)前線(xiàn)程是否大于 corePoolSize // 當(dāng) core 為假, 那么就判斷當(dāng)前線(xiàn)程數(shù)是否大于 maximumPoolSize // 這里的 for 循環(huán)是一個(gè)自旋CAS(CompareAndSwap)操作, 用于確保多線(xiàn)程環(huán)境下的正確性 if (wc >= CAPACITY || wc >= (core ? corePoolSize : ma)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
首先在 addWorker 的一開(kāi)始, 有一個(gè) for 循環(huán), 用于判斷當(dāng)前是否可以添加新的 Worker 線(xiàn)程. 它的邏輯如下:
如果傳入的 core 為真, 那么判斷當(dāng)前的線(xiàn)程數(shù)是否大于 corePoolSize, 如果大于, 則不能新建 Worker 線(xiàn)程, 返回 false.
如果傳入的 core 為假, 那么判斷當(dāng)前的線(xiàn)程數(shù)是否大于 maximumPoolSize, 如果大于, 則不能新建 Worker 線(xiàn)程, 返回 false.
如果條件符合, 那么在 for 循環(huán)內(nèi), 又有一個(gè)自旋CAS 更新邏輯, 用于遞增當(dāng)前的線(xiàn)程數(shù), 即 compareAndIncrementWorkerCount(c), 這個(gè)方法會(huì)原子地更新 ctl 的值, 將當(dāng)前線(xiàn)程數(shù)的值遞增一.
addWorker 接下來(lái)有一個(gè) try...finally 語(yǔ)句塊, 這里就是實(shí)際上的創(chuàng)建線(xiàn)程、啟動(dòng)線(xiàn)程、添加線(xiàn)程到線(xiàn)程池中的工作了.
接下來(lái)我們看任務(wù)的真正執(zhí)行runWorker
語(yǔ)義:執(zhí)行runWorker,調(diào)用Runnable的run方法,再其外圍包裝了中斷的策略
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // 如果線(xiàn)程池正在停止,確保其中斷 // 如果不是,確保其不中斷 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }reject
語(yǔ)義:提交不了線(xiàn)程池時(shí),拒絕策略
接下來(lái)我們看線(xiàn)程提交被拒絕的策略reject
final void reject(Runnable command) { handler.rejectedExecution(command, this); }
RejectedExecutionHandler 接口有四種實(shí)現(xiàn)
AbortPolicy;拒絕,拋出RejectedExecutionException
CallerRunsPolicy:如果被拒絕,在當(dāng)前線(xiàn)程執(zhí)行任務(wù),
RejectedExecutionHandler:靜靜的拒絕,什么都不做
DiscardOldestPolicy:丟棄最老的未處理的請(qǐng)求,重試提交當(dāng)前請(qǐng)求
shutdown語(yǔ)義:不接受新任務(wù),但是處理隊(duì)列中的任務(wù)
shutdown方法主要就是設(shè)置線(xiàn)程池狀態(tài),設(shè)置空閑的worker的中斷
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); interruptIdleWorkers(); onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); }shutdownNow
shutdownNow:不接受新任務(wù),也不處理隊(duì)列中的任務(wù),還會(huì)中斷已經(jīng)進(jìn)行中的任務(wù)
shutdownNow方法主要就是設(shè)置線(xiàn)程池狀態(tài),設(shè)置所有worker的中斷
public ListshutdownNow() { List tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); interruptWorkers(); tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
本文嚴(yán)重參考Java ThreadPoolExecutor 線(xiàn)程池源碼分析
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/70095.html
摘要:那么線(xiàn)程池到底是怎么利用類(lèi)來(lái)實(shí)現(xiàn)持續(xù)不斷地接收提交的任務(wù)并執(zhí)行的呢接下來(lái),我們通過(guò)的源代碼來(lái)一步一步抽絲剝繭,揭開(kāi)線(xiàn)程池運(yùn)行模型的神秘面紗。 在上一篇文章《從0到1玩轉(zhuǎn)線(xiàn)程池》中,我們了解了線(xiàn)程池的使用方法,以及向線(xiàn)程池中提交任務(wù)的完整流程和ThreadPoolExecutor.execute方法的源代碼。在這篇文章中,我們將會(huì)從頭閱讀線(xiàn)程池ThreadPoolExecutor類(lèi)的源代...
摘要:最后,我們會(huì)通過(guò)對(duì)源代碼的剖析深入了解線(xiàn)程池的運(yùn)行過(guò)程和具體設(shè)計(jì),真正達(dá)到知其然而知其所以然的水平。創(chuàng)建線(xiàn)程池既然線(xiàn)程池是一個(gè)類(lèi),那么最直接的使用方法一定是一個(gè)類(lèi)的對(duì)象,例如。單線(xiàn)程線(xiàn)程池單線(xiàn)程線(xiàn)程 我們一般不會(huì)選擇直接使用線(xiàn)程類(lèi)Thread進(jìn)行多線(xiàn)程編程,而是使用更方便的線(xiàn)程池來(lái)進(jìn)行任務(wù)的調(diào)度和管理。線(xiàn)程池就像共享單車(chē),我們只要在我們有需要的時(shí)候去獲取就可以了。甚至可以說(shuō)線(xiàn)程池更棒,...
摘要:提交任務(wù)當(dāng)創(chuàng)建了一個(gè)線(xiàn)程池之后我們就可以將任務(wù)提交到線(xiàn)程池中執(zhí)行了。提交任務(wù)到線(xiàn)程池中相當(dāng)簡(jiǎn)單,我們只要把原來(lái)傳入類(lèi)構(gòu)造器的對(duì)象傳入線(xiàn)程池的方法或者方法就可以了。 我們一般不會(huì)選擇直接使用線(xiàn)程類(lèi)Thread進(jìn)行多線(xiàn)程編程,而是使用更方便的線(xiàn)程池來(lái)進(jìn)行任務(wù)的調(diào)度和管理。線(xiàn)程池就像共享單車(chē),我們只要在我們有需要的時(shí)候去獲取就可以了。甚至可以說(shuō)線(xiàn)程池更棒,我們只需要把任務(wù)提交給它,它就會(huì)在合...
摘要:參數(shù)說(shuō)明,線(xiàn)程池保留的最小線(xiàn)程數(shù)。,線(xiàn)程池中允許擁有的最大線(xiàn)程數(shù)。,線(xiàn)程池的運(yùn)行狀態(tài)。除非線(xiàn)程池狀態(tài)發(fā)生了變化,發(fā)退回到外層循環(huán)重新執(zhí)行,判斷線(xiàn)程池的狀態(tài)。是線(xiàn)程池的核心控制狀態(tài),包含的線(xiàn)程池運(yùn)行狀態(tài)和有效線(xiàn)程數(shù)。 Java是一門(mén)多線(xiàn)程的語(yǔ)言,基本上生產(chǎn)環(huán)境的Java項(xiàng)目都離不開(kāi)多線(xiàn)程。而線(xiàn)程則是其中最重要的系統(tǒng)資源之一,如果這個(gè)資源利用得不好,很容易導(dǎo)致程序低效率,甚至是出問(wèn)題。 有...
摘要:為了讓大家理解線(xiàn)程池的整個(gè)設(shè)計(jì)方案,我會(huì)按照的設(shè)計(jì)思路來(lái)多說(shuō)一些相關(guān)的東西。也是因?yàn)榫€(xiàn)程池的需要,所以才有了這個(gè)接口。 線(xiàn)程池是非常重要的工具,如果你要成為一個(gè)好的工程師,還是得比較好地掌握這個(gè)知識(shí)。即使你為了謀生,也要知道,這基本上是面試必問(wèn)的題目,而且面試官很容易從被面試者的回答中捕捉到被面試者的技術(shù)水平。 本文略長(zhǎng),建議在 pc 上閱讀,邊看文章邊翻源碼(Java7 和 Java...
閱讀 3869·2021-10-08 10:05
閱讀 2949·2021-09-27 13:57
閱讀 2685·2019-08-29 11:32
閱讀 1010·2019-08-28 18:18
閱讀 1291·2019-08-28 18:05
閱讀 1987·2019-08-26 13:39
閱讀 867·2019-08-26 11:37
閱讀 2046·2019-08-26 10:37