ThreadPoolExecutor 源代碼解析(base jdk1.8)

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)化,重點是思想。這些都是非常通用,非常有價值的。

。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容