ThreadPoolExecutor 是java線程池的默認(rèn)實現(xiàn)。本文從源代碼的角度來解析線程池,后續(xù)會出一個系列的源代碼解析。
1.線程池初始化
下面是線程池最基礎(chǔ)的初始化函數(shù)
public ThreadPoolExecutor(int corePoolSize,
? ? ? ? ? ? ? ? ? ? ? ? ? int maximumPoolSize,
? ? ? ? ? ? ? ? ? ? ? ? ? long keepAliveTime,
? ? ? ? ? ? ? ? ? ? ? ? ? TimeUnit unit,
? ? ? ? ? ? ? ? ? ? ? ? ? BlockingQueue workQueue,
? ? ? ? ? ? ? ? ? ? ? ? ? ThreadFactory threadFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? RejectedExecutionHandler handler) {
? ? this.corePoolSize = corePoolSize;
? ? this.maximumPoolSize = maximumPoolSize;
? ? this.workQueue = workQueue;
? ? this.keepAliveTime = unit.toNanos(keepAliveTime);
? ? this.threadFactory = threadFactory;
? ? this.handler = handler;
}
corePoolSize :核心線程數(shù),設(shè)置在隊列不滿的情況下線程池中的最大線程個數(shù)
maximumPoolSize:工作隊列滿了之后最大的線程個數(shù)
workQueue:翻譯是工作隊列,因為是任務(wù)的提交是先到隊列,然后才到線程處理的,不管線程是否空閑
threadFactory:線程工廠,負(fù)責(zé)創(chuàng)建線程,包括線程優(yōu)先級和線程名稱,線程名稱非常重要,這就很好給現(xiàn)有線程分類了
handler:RejectedExecutionHandler接口,是線程池處理不過來的任務(wù)的的處理策略。處理不過來的這個標(biāo)準(zhǔn)是工作隊列滿了并且線程池當(dāng)前線程數(shù)漲到maximumPoolSize。
keepAliveTime:線程等待新任務(wù)的超時機制的時長,如果allowCoreThreadTimeOut為false,線程就是一直等新任務(wù)的xai
2.線程池中的屬性的默認(rèn)值
線程池狀態(tài)
? ? private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
? ? private static final int COUNT_BITS = Integer.SIZE - 3;
? ? private static final int CAPACITY? = (1 << COUNT_BITS) - 1;
? ? // runState is stored in the high-order bits
? ? private static final int RUNNING? ? = -1 << COUNT_BITS;//正常接收新的任務(wù)和處理隊列中的任務(wù)
? ? private static final int SHUTDOWN? =? 0 << COUNT_BITS;//不接受新的任務(wù),但是處理隊列中的任務(wù)
? ? private static final int STOP? ? ? =? 1 << COUNT_BITS;//不接受新的任務(wù),也不處理隊列中的任務(wù)
? ? private static final int TIDYING? ? =? 2 << COUNT_BITS;//所有的任務(wù)多處于中斷狀態(tài),線程將要進行中斷
? ? private static final int TERMINATED =? 3 << COUNT_BITS;//中斷完成
? ? ? ? ?線程池狀態(tài)有五種,代碼片段中,這個是用的整形最大值-1的高三位來做記錄。這個是常用的比直接用整形值要簡單并且計算更快。jdk1.6的實現(xiàn)使用的就是整形字段標(biāo)識的。這算是jdk1.8性能的一個提升吧。
3.默認(rèn)線程工廠
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 = "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;
? ? ? ? }
? ? }
默認(rèn)的線程工廠就是設(shè)置一下線程的優(yōu)先級和線程的名字,這里面有兩個原子類標(biāo)識的變量一個是threadNumber標(biāo)識的是當(dāng)前線程在這個線程池中的標(biāo)識。poolNumber標(biāo)識的是不同的線程池。這里不建議使用這個默認(rèn)線程工廠,因為這個兩個名字僅僅標(biāo)識了不同,但是沒有實際的業(yè)務(wù)意義。推薦使用ThreadPoolTaskExecutor spring中的線程池的實現(xiàn),這里面的線程名字的前綴是這個線程池的beanName
4.RejectedExecutionHandler 線程池的拒絕策略
public interface RejectedExecutionHandler {
? ? /**
? ? * Method that may be invoked by a {@link ThreadPoolExecutor} when
? ? * {@link ThreadPoolExecutor#execute execute} cannot accept a
? ? * task.? This may occur when no more threads or queue slots are
? ? * available because their bounds would be exceeded, or upon
? ? * shutdown of the Executor.
? ? *
? ? *
In the absence of other alternatives, the method may throw
? ? * an unchecked {@link RejectedExecutionException}, which will be
? ? * propagated to the caller of {@code execute}.
? ? *
? ? * @param r the runnable task requested to be executed
? ? * @param executor the executor attempting to execute this task
? ? * @throws RejectedExecutionException if there is no remedy
? ? */
? ? void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
?拒絕策略實現(xiàn)有四種,線程池的默認(rèn)實現(xiàn)是AbortPolicy 這種就是會直接拋出異常
CallerRunsPolicy 是直接調(diào)用run方法就直接串行執(zhí)行了。
DiscardOldestPolicy 是將隊列中第一個任務(wù)poll出,然后將這個任務(wù)放到隊尾。隊列是先進先出的,一般不會有從插隊行為
DiscardPolicy 是本寶寶什么也沒有做。
編寫自定義的拒絕策略也是很簡單,入?yún)⑹钱?dāng)前任務(wù)和當(dāng)前線程池。擁有當(dāng)前線程池這個引用,想干啥都行了,就是注意當(dāng)前是多線程環(huán)境,避免出現(xiàn)并發(fā)問題就行。
5.核心方法
就不畫流程圖了按照順序說了
?1.submit
這個方法就是包裝一下Runnable接口然后調(diào)用excute方法
public Future submit(Runnable task) {
? ? if (task == null) throw new NullPointerException();
? ? RunnableFuture ftask = newTaskFor(task, null);
? ? execute(ftask);
? ? return ftask;
}
2.excute
int c = ctl.get();
? ? ? ? //第一種情況需要創(chuàng)建新的線程,計算出當(dāng)前的線程數(shù)量和核心線程數(shù)比較,如果小于就創(chuàng)建一個新的線程并且執(zhí)行這個任務(wù)。
? ? ? ? // 這類線程被稱為worker
? ? ? ? //失敗的場景就是要創(chuàng)建線程的時候發(fā)現(xiàn)超過了核心線程數(shù),因為是并發(fā)場景,就是有人手快所以就失敗了
? ? ? ? // 那么這里c=ctl.get() 這里是必須要寫的一行代碼,因為這個值已經(jīng)被更改了,需要調(diào)用一下get方法同步
? ? ? ? //不理解的參見原子類的使用
? ? ? ? if (workerCountOf(c) < corePoolSize) {
? ? ? ? ? ? if (addWorker(command, true))
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? c = ctl.get();
? ? ? ? }
? ? ? ? //第二種情況,向隊列提交任務(wù)提交任務(wù),
? ? ? ? if (isRunning(c) && workQueue.offer(command)) {
? ? ? ? ? ? int recheck = ctl.get();
? ? ? ? ? ? //提交成功之后做一個雙檢查,因為這個時候可能會發(fā)生線程池關(guān)閉了。上面也是刷新一下ctl的值
? ? ? ? ? ? //確實線程池關(guān)閉了,就從隊列中移除,然后執(zhí)行拒接策略
? ? ? ? ? ? if (! isRunning(recheck) && remove(command))
? ? ? ? ? ? ? ? reject(command);
? ? ? ? ? ? //如果線程池沒有關(guān)閉,但是中沒有線程,就創(chuàng)建一個線程
? ? ? ? ? ? else if (workerCountOf(recheck) == 0)
? ? ? ? ? ? ? ? addWorker(null, false);
? ? ? ? }
? ? ? ? //如果提交任務(wù)失敗,嘗試提高線程數(shù)量到最大線程數(shù),不行就執(zhí)行拒絕策略
? ? ? ? else if (!addWorker(command, false))
? ? ? ? ? ? reject(command);
3.addworker
private boolean addWorker(Runnable firstTask, boolean core) {
? ? ? ? retry:
? ? ? ? for (;;) {
? ? ? ? ? ? int c = ctl.get();
? ? ? ? ? ? int rs = runStateOf(c);
? ? ? ? ? ? // 對線程狀態(tài)和空隊列的檢查
? ? ? ? ? ? if (rs >= SHUTDOWN &&
? ? ? ? ? ? ? ? ? ? ! (rs == SHUTDOWN &&
? ? ? ? ? ? ? ? ? ? ? ? ? ? firstTask == null &&
? ? ? ? ? ? ? ? ? ? ? ? ? ? ! workQueue.isEmpty()))
? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? for (;;) {
? ? ? ? ? ? ? ? int wc = workerCountOf(c);
? ? ? ? ? ? ? ? if (wc >= CAPACITY ||
? ? ? ? ? ? ? ? ? ? ? ? wc >= (core ? corePoolSize : maximumPoolSize))
? ? ? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? ? ? //線程數(shù)增加CAS成功跳出循環(huán)如果失敗刷新ctl的值然后繼續(xù)循環(huán)
? ? ? ? ? ? ? ? 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.
? ? ? ? ? ? ? ? ? ? //獲取線程池狀態(tài)不在結(jié)束狀態(tài)把worker加入進隊列
? ? ? ? ? ? ? ? ? ? // 為什么加鎖呢,我覺得是因為workers 這個對象是hashset是線程不安全的
? ? ? ? ? ? ? ? ? ? //largestPoolSize也是int類型。不同線程指令的進行隨機
? ? ? ? ? ? ? ? ? ? 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();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? //判斷線程是否添加成功,然后啟動線程,再標(biāo)識線程啟動成功
? ? ? ? ? ? ? ? if (workerAdded) {
? ? ? ? ? ? ? ? ? ? t.start();
? ? ? ? ? ? ? ? ? ? workerStarted = true;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? //判斷線程是否啟動成功,如果沒有成功把該線程從workers中移除
? ? ? ? } finally {
? ? ? ? ? ? if (! workerStarted)
? ? ? ? ? ? ? ? addWorkerFailed(w);
? ? ? ? }
? ? ? ? return workerStarted;
? ? }
3.Worker
private final class Worker
? ? extends AbstractQueuedSynchronizer
? ? implements Runnable
? ? }
? ? private static final long serialVersionUID = 6138294804551838833L;
? ? final Thread thread;
? ? Runnable firstTask;
? ? volatile long completedTasks;
Worker 繼承自AbstractQueuedSynchronizer 實現(xiàn)Runnable 。也就是說這個worker自身會有鎖特性,它在做中斷的時候會給自身加個鎖。也會在跑任務(wù)的時候加個鎖,防止這個時候被中斷。然后封裝成線程。run方法就是worker跑任務(wù)的方法入口,執(zhí)行的是runWorker方法
? /** worker的run方法,邏輯主要執(zhí)行的runWorker? */
? ? ? ? public void run() {
? ? ? ? ? ? runWorker(this);
? ? ? ? }
前面都是一些判斷,getTask是獲取任務(wù)的重要方法,task.run 是任務(wù)執(zhí)行?processWorkerExit 是跑任務(wù)的時候接受到了線程中拋出的異常,中斷了這個線程之后的處理邏輯
final void runWorker(Worker w) {
? ? ? ? Thread wt = Thread.currentThread();
? ? ? ? Runnable task = w.firstTask;
? ? ? ? w.firstTask = null;
? ? ? ? w.unlock(); // allow interrupts
? ? ? ? boolean completedAbruptly = true;
? ? ? ? try {
? ? ? ? ? ? //getTask方法從隊列當(dāng)中取任務(wù)
? ? ? ? ? ? while (task != null || (task = getTask()) != null) {
? ? ? ? ? ? ? ? //正在跑任務(wù)加鎖防止被中斷
? ? ? ? ? ? ? ? w.lock();
? ? ? ? ? ? ? ? //判斷線程池狀態(tài)和中斷信號
? ? ? ? ? ? ? ? if ((runStateAtLeast(ctl.get(), STOP) ||
? ? ? ? ? ? ? ? ? ? ? ? (Thread.interrupted() &&
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? runStateAtLeast(ctl.get(), STOP))) &&
? ? ? ? ? ? ? ? ? ? ? ? !wt.isInterrupted())
? ? ? ? ? ? ? ? ? ? wt.interrupt();
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? //執(zhí)行之前執(zhí)行的方法,目前沒有實現(xiàn),需要繼承線程池之后實現(xiàn)
? ? ? ? ? ? ? ? ? ? beforeExecute(wt, task);
? ? ? ? ? ? ? ? ? ? Throwable thrown = null;
? ? ? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? ? ? //核心方法跑任務(wù)
? ? ? ? ? ? ? ? ? ? ? ? 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 {
? ? ? ? ? ? ? ? ? ? ? ? //任務(wù)接受到異常之后執(zhí)行的方法,目前沒有實現(xiàn),需要繼承線程池之后實現(xiàn)
? ? ? ? ? ? ? ? ? ? ? ? afterExecute(task, thrown);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? ? ? task = null;
? ? ? ? ? ? ? ? ? ? w.completedTasks++;
? ? ? ? ? ? ? ? ? ? w.unlock();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? completedAbruptly = false;
? ? ? ? } finally {
? ? ? ? ? ? //走到這里就是跑的任務(wù)接受到異常退出了,進行的邏輯,
? ? ? ? ? ? processWorkerExit(w, completedAbruptly);
? ? ? ? }
gettask方法重要的邏輯是判斷一些狀態(tài),然后從任務(wù)隊列中拉去任務(wù),這段代碼很重要,復(fù)雜的涉及到兩個邏輯,一個是任務(wù)隊列當(dāng)中沒有任務(wù)之后,當(dāng)前線程去留問題,另一個是
private Runnable getTask() {
? ? ? ? boolean timedOut = false; // 上一個任務(wù)再拉去的時候有沒有超時
? ? ? ? for (;;) {
? ? ? ? ? ? int c = ctl.get();
? ? ? ? ? ? int rs = runStateOf(c);
? ? ? ? ? ? // 檢查線程池狀態(tài)為shutdown或者隊列為空
? ? ? ? ? ? if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
? ? ? ? ? ? ? ? //線程數(shù)減1操作
? ? ? ? ? ? ? ? decrementWorkerCount();
? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? }
? ? ? ? ? ? int wc = workerCountOf(c);
? ? ? ? ? ? // 判斷這個參數(shù)是否允許在核心線程有超時機制,或者當(dāng)前線程數(shù)大于核心線程數(shù)
? ? ? ? ? ? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
? ? ? ? ? ? // 判斷當(dāng)前線程數(shù)是否大于最大線程數(shù) 或者 這個timeOut標(biāo)記是上一個任務(wù)有沒有超時,如果有這個任務(wù)就很可能不跑了直接返回了
? ? ? ? ? ? //這個取決于這個核心線程有沒有超時機制或者當(dāng)前線程數(shù)大于核心線程數(shù),都有,那就返回退出該線程了。
? ? ? ? ? ? // 并且當(dāng)前線程確實為空并且當(dāng)前線程池還有活躍的線程
? ? ? ? ? ? if ((wc > maximumPoolSize || (timed && timedOut))
? ? ? ? ? ? ? ? ? ? && (wc > 1 || workQueue.isEmpty())) {
? ? ? ? ? ? ? ? //進行cas操作,如果失敗,就繼續(xù)跑任務(wù)
? ? ? ? ? ? ? ? if (compareAndDecrementWorkerCount(c))
? ? ? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? }
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? //線程池的超時機制其實是等待任務(wù)從任務(wù)隊列中提交過來的時間,沒有計算線程的執(zhí)行時間
? ? ? ? ? ? ? ? Runnable r = timed ?
? ? ? ? ? ? ? ? ? ? ? ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
? ? ? ? ? ? ? ? ? ? ? ? workQueue.take();
? ? ? ? ? ? ? ? if (r != null)
? ? ? ? ? ? ? ? ? ? return r;
? ? ? ? ? ? ? ? timedOut = true;
? ? ? ? ? ? } catch (InterruptedException retry) {
? ? ? ? ? ? ? ? timedOut = false;
? ? ? ? ? ? }
? ? ? ? }
? ? }
processWorkerExit方法是處理線程中斷之后的邏輯,要不就是移除引用之后就結(jié)束了,要不就是再添加新的線程補充上來
//worker 響應(yīng)異常之后的處理,如果worker的死亡是用戶拋出來的異常這個值就是true
? ? private void processWorkerExit(Worker w, boolean completedAbruptly) {
? ? ? ? if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
? ? ? ? ? ? decrementWorkerCount();
? ? ? ? //從線程池中移除該隊列
? ? ? ? final ReentrantLock mainLock = this.mainLock;
? ? ? ? mainLock.lock();
? ? ? ? try {
? ? ? ? ? ? completedTaskCount += w.completedTasks;
? ? ? ? ? ? workers.remove(w);
? ? ? ? } finally {
? ? ? ? ? ? mainLock.unlock();
? ? ? ? }
? ? ? ? tryTerminate();
? ? ? ? int c = ctl.get();
? ? ? ? if (runStateLessThan(c, STOP)) {
? ? ? ? ? ? //如果是用戶線程拋出來的異常,線程池會立即補上這個數(shù)量,相當(dāng)于立刻替換這個工人
? ? ? ? ? ? if (!completedAbruptly) {
? ? ? ? ? ? ? ? //如果不是用戶拋出的異常線程池會判斷允許超時機制存在么,存在,并且確實任務(wù)隊列沒有任務(wù)返回不再加新的線程了
? ? ? ? ? ? ? ? // 如果是線程數(shù)大于核心線程數(shù),也是返回不加新的線程了
? ? ? ? ? ? ? ? int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
? ? ? ? ? ? ? ? if (min == 0 && ! workQueue.isEmpty())
? ? ? ? ? ? ? ? ? ? min = 1;
? ? ? ? ? ? ? ? if (workerCountOf(c) >= min)
? ? ? ? ? ? ? ? ? ? return; // replacement not needed
? ? ? ? ? ? }
? ? ? ? ? ? //加新的線程補充之前死掉的線程
? ? ? ? ? ? addWorker(null, false);
? ? ? ? }
? ? }
后記
線程池的核心方法差不多都介紹完了,jdk中的代碼確實非常經(jīng)典,每一段代碼都值得細(xì)細(xì)的去品味。線程池一個批量任務(wù)的并發(fā)框架,包含線程管理,任務(wù)管理,超時機制,在并發(fā)的場景,還會學(xué)到一些并發(fā)場景下的對象處理手段。看源代碼首先就是先要明白這個段代碼它想完成一個什么樣的功能,再不看源代碼的情景下,去猜內(nèi)部實現(xiàn)邏輯,然后再去看代碼,反復(fù)印證。這樣你不僅是懂了這個代碼實現(xiàn),也會懂這個的設(shè)計原理,設(shè)計模式。這類的通用類型的設(shè)計思想。而且還要比較低版本和高版本之間的對比,知道這個升級是為什么問題,會有優(yōu)化,重點是思想。這些都是非常通用,非常有價值的。
。