天天看點

如何吃透一個Java項目?(附學習實踐)

首先,嘗試分析下題主感到空虛、似懂非懂的原因,從問題描述來看原因可能有以下幾方面:

▐  目标不清晰

在項目學習之前,是否有認真梳理和思考過,希望通過項目學習到哪些技術、重點需掌握哪些知識點?這些知識點又屬于自己技術體系中哪個環節,是需要必須熟練掌握還是了解原理即可?相信隻有明确目标之後才有學習側重點和方向。

▐  學習方法

項目學習過程中,是否有帶着問題和思考?比如項目核心需要解決的問題場景、使用了哪些技術方案,為什麼需要這些技術,方案選擇考慮主要有哪些?系統子產品這樣分層和實作的好處是?這個方法的實作,性能是否可以進一步優化等等。

如果隻是純粹跟着視訊将項目代碼機械敲一遍,我認為跟練習打字沒任何差別,寫出來的代碼也是沒有靈魂如行屍走肉。我相信隻有結合自己的思考和了解,才可能賦予新的靈魂,做到知其然知其是以然,相關知識點也才能真正轉化為自己的技術。

▐  複習與應用

紙上得來終覺淺,絕知此事要躬行,相信對程式設計而言更是如此,唯有實踐才能出真知。對項目中學到的相關技術、知識點需要在不同場景反複練習和應用,并對過程中遇到的問題不斷總結和反思。

其次,回到題主問題,如何吃透一個Java項目?從個人經驗來看,大緻可以從以下幾方面入手:

▐  項目背景了解

學習之前,先對項目業務背景和技術體系做大緻的了解,這點非常重要,一是為了解項目核心要解決問題域,二是知道系統涉及哪些技術體系,這樣在學習之前可以有相關技術知識準備,以便更輕松高效學習。另外,學習完之後也可以清楚知道,什麼樣問題可以使用什麼技術、什麼方案來解決、如何解決的。

▐  系統設計文檔學習

對項目和系統大概了解之後,可以開始對系統設計文檔熟悉,建議按照架構文檔、概要設計、詳細設計方式遞進。通過設計文檔的學習,可以快速對各系統子產品有個架構性認識,知道各系統職責、邊界、如何互動、系統核心模型等等。

對于設計文檔的學習,切不可走馬觀花,一定要帶着問題和思考。比如項目背景中的核心業務問題,架構師是如何轉化成技術落地,方案為什麼要這樣設計,模型為什麼要這樣抽象,這樣做的好處是什麼等等?同時,對不了解的問題做需好筆記,以便後續向老師或其他同僚請教或讨論等等。

▐  系統熟悉和代碼閱讀

通過設計文檔的學習,對系統設計有整體了解之後,接下來就可以結合業務場景、相關問題去看代碼如何實作了。不過代碼閱讀,也需要注意方式方法,切不可陷入代碼細節,應該自頂向下、分層分子產品的閱讀,以先整體、後子產品、單功能點的方式層層遞進。先快速走讀整個代碼子產品邏輯,然後再精讀某個類、方法的實作。

代碼閱讀過程中,建議一邊閱讀一邊整理相關代碼子產品、流程分支、互動時序,以及類圖等,以便更好了解,有些IDE工具也可根據代碼自動生成,比如IntelliJ IDEA。

代碼閱讀除了關注具體功能的實作之外,更重要的是需要關心代碼設計上的思路和原理、性能考究、設計模式、以及設計原則的應用等。同樣,閱讀代碼注釋也非常重要,在研究一個API或方法實作時,先認真閱讀代碼注釋會讓你事半功倍,盡可能不要做從代碼中反推邏輯和功能的事情。

最後,對于核心功能代碼建議分子產品精讀,不明白部分可借助代碼調試。

然後,對于技術學習這塊我給幾點個人建議,以供題主參考:

▐  制定學習規劃

梳理一份适合自己的技術規劃,并制定明确的學習路線和計劃,讓學習更有方向和重點。同樣在視訊課程的選擇上也會更清晰,知道什麼樣視訊該學、什麼不該學,也不容易感到迷茫和空虛。如今網上各種學習資料、視訊汗牛充棟,學會如何篩選有效、适合自己的資訊非常重要。

▐  思考與練習

對于技術程式設計,無捷徑可言,思考和練習都非常重要,需要不斷學習、思考、實踐反複操練。從了解、會用、知原理、優化不斷演進。結合學習計劃,可以給自己制定不同挑戰,比如學習spring可以嘗試自己實作一個ioc容器等等。另外,工作或學習過程中遇到的問題,也是你快速提升技術能力的一個好方法,也請珍惜你遇到的每個問題的機會。時間允許的話,也請盡可能去幫助别人解答問題,像stackoverflow就是個非常不錯的選擇,幫助别人的同時提升自己。

▐  分享與交流

保持思考總結的習慣,将學到的技術多與人分享交流,教學相長。多與優秀的程式員一起、多參與優秀的開源項目等。

最後,我再以我們團隊Dubbo核心開發@哲良 大神的另一開源架構

TransmittableThreadLocal(TTL)

為例,來講解下我們該如何學習和快速掌握一個項目。

結合上文所述,首先我會将TTL項目相關

文檔

issues清單

認真閱讀一遍,讓自己對項目能有個大體的認識,并梳理出項目一些關鍵資訊,比如:

▐  核心要解決的問題

用于解決「線上程池或線程會被複用情況下,如何解決線程ThreadLocal傳值問題」

▐  有哪些典型業務場景

    • 分布式跟蹤系統或全鍊路壓測(即鍊路打标)
    • 日志收集記錄系統上下文
    • Session級Cache
    • 應用容器或上層架構跨應用代碼給下層SDK傳遞資訊

▐  使用到的技術

有線程、線程池、ThreadLocal、InheritableThreadLocal、并發、線程安全等。

然後,再結合使用文檔編寫幾個測試demo,通過程式代碼練習和架構使用,一步步加深對架構的了解。比如我這裡首先會拿TTL與原生JDK InheritableThreadLocal進行不同比較,體驗兩者的核心差別。

public class ThreadLocalTest {

public class ThreadLocalTest {

    private static final AtomicInteger ID_SEQ = new AtomicInteger();
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1, r -> new Thread(r, "TTL-TEST-" + ID_SEQ.getAndIncrement()));

    //
    private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();

    //⑴ 聲明TransmittableThreadLocal類型的ThreadLocal
    //private static ThreadLocal<String> THREAD_LOCAL = new TransmittableThreadLocal<>();
    public static void testThreadLocal() throws InterruptedException {
        try {
            //doSomething()...
            THREAD_LOCAL.set("set-task-init-value");
            //
            Runnable task1 = () -> {
                try {
                    String manTaskCtx = THREAD_LOCAL.get();
                    System.out.println("task1:" + Thread.currentThread() + ", get ctx:" + manTaskCtx);
                    THREAD_LOCAL.set("task1-set-value");
                } finally {
                    THREAD_LOCAL.remove();
                }
            };
            EXECUTOR.submit(task1);

            //doSomething....
            TimeUnit.SECONDS.sleep(3);

            //⑵ 設定期望task2可擷取的上下文
            THREAD_LOCAL.set("main-task-value");

            //⑶ task2的異步任務邏輯中期望擷取⑵中的上下文
            Runnable task2 = () -> {
                String manTaskCtx = THREAD_LOCAL.get();
                System.out.println("task2:" + Thread.currentThread() + ", get ctx :" + manTaskCtx);
            };
            //⑷ 轉換為TransmittableThreadLocal 增強的Runnable
            //task2 = TtlRunnable.get(task2);
            EXECUTOR.submit(task2);
        }finally {
            THREAD_LOCAL.remove();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        testThreadLocal();
    }
}

//InheritableThreadLocal 運作結果:
task1:Thread[TTL-TEST-0,5,main], get ctx:set-task-init-value
task2:Thread[TTL-TEST-0,5,main], get ctx :null
    
//TransmittableThreadLocal 運作結果
task1:Thread[TTL-TEST-0,5,main], get ctx:set-task-init-value
task2:Thread[TTL-TEST-0,5,main], get ctx :main-task-value    
    
          

通過代碼運作結果,我們可以直覺看到使用JDK原生InheritableThreadLocal,在task2異步任務中是無法正确擷取代碼⑵處所設定的上下文參數,隻有改用TransmittableThreadLocal之後,程式才如我們預期正常擷取。

不難發現,由JDK原生ThreadLocal切換到TransmittableThreadLocal,隻需要做極少量的代碼适配即可。

//private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();
//⑴ 聲明TransmittableThreadLocal類型的ThreadLocal
private static ThreadLocal<String> THREAD_LOCAL = new TransmittableThreadLocal<>();

...
//⑷ 轉換為TransmittableThreadLocal 增強的Runnable
task2 = TtlRunnable.get(task2);      

相信看到這裡我們都會不禁想問,為什麼隻需要簡單的更改兩行代碼,就可以平滑實作上下文透傳?TTL架構背後具體都做了哪些工作,到底是怎麼實作的呢?相信你和我一樣都會比較好奇,也一定有想立馬閱讀源碼一探究竟的沖動。

不過,通常這個時候,我并不會一頭紮進源碼,一般都會先做幾項準備工作,一是回到設計文檔再仔細的閱讀下相關實作方案,把關鍵流程和原理了解清楚;二是把涉及到的技術體相關的基礎知識再複習或學習一遍,以避免由于一些基礎知識原理的不了解,導緻源碼無法深入研究或花費大量精力。像這裡如果我對Thread、ThreadLocal、InheritableThreadLocal、線程池等相關知識不熟悉的話,一定會把相關知識先學習一遍,比如ThreadLocal基本原理、底層資料結構、InheritableThreadLocal如何實作父子線程傳遞等等。

假設這裡你對這些知識都已掌握,如果不熟悉,網上相關介紹文章也早已是汗牛充棟,你搜尋學習下即可。這裡我們先帶着到底如何實作的這個疑問,一起來探究下核心源碼實作。

首先把源碼clone下來導入IDE,然後結合文檔把系統工程結構和各功能子產品職責快速熟悉一遍,然後結合文檔和Demo找到關鍵接口和實作類,利用IDE把相關類圖結構生成出來,以便快速了解類之間關系。非常不錯,TTL整體代碼非常精練、命名和包資訊描述也都非正常範和清晰,我們可以快速圈出來。

如何吃透一個Java項目?(附學習實踐)

從類圖中我們可以清晰看到核心關鍵類TransmittableThreadLocal是從ThreadLocal繼承而來,這樣的好處是不破壞ThreadLocal原生能力的同時還可增強和擴充自有能力,也可保證業務代碼原有互操作性和最小改動。

然後結合Demo代碼,我們不難發現使用TTL主要有三個步驟,TransmittableThreadLocal聲明、set、remove方法的調用。根據整個使用流程和方法調用棧,我們也可以很友善梳理出整個代碼處理初始化、調用時序。

(這裡借用官方原圖)

如何吃透一個Java項目?(附學習實踐)

通過流程圖,我們可以清晰看到TTL核心流程和原理是通過TransmittableThreadLocal.Transmitter 抓取目前線程的所有TTL值并在其他線程進行回放,然後在回放線程執行完業務操作後,再恢複為回放線程原來的TTL值。

TransmittableThreadLocal.Transmitter提供了所有TTL值的抓取、回放和恢複方法(即CRR操作): 

capture方法:抓取線程(線程A)的所有TTL值。 

replay方法:在另一個線程(線程B)中,回放在capture方法中抓取的TTL值,并傳回 回放前TTL值的備份 

restore方法:恢複線程B執行replay方法之前的TTL值(即備份)

弄明白核心流程和原理後,我們現在來分析下相關核心代碼,在聲明TransmittableThreadLocal變量時,我們會發現架構初始化了一個類級别的變量holder用于存儲使用者設定的所有ttl上下文,也是為了後續執行capture抓取時使用。​

// Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        the value of WeakHashMap is *always* null, and never used.
    //    2.2 WeakHashMap support *null* value.
    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
        new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
        @Override
        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
            return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
        }
        @Override
        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
            return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
        }
    };
    
    /**
     * see {@link InheritableThreadLocal#set}
     */
    @Override
    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && null == value) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }

    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }      

結合set方法實作來看,我們會發現holder變量設計的非常巧妙,業務設定的上下文value部分繼續複用ThreadLocal原有資料結構ThreadLocalMap來存儲( super.set(value));capture的資料源利用holder進行引用存儲(addThisToHolder put this)。這樣的好處是既可保持ThreadLocal資料存儲原有的封裝性,又很好實作擴充。除此之外,holder還有其他設計考究,這裡抛出來大家可以思考下:

  1. 為什麼holder需要設計成static final類級别變量?
  2. ttl變量的存儲為什麼需要使用WeakHashMap,而不是hashmap或其他?

然後我們再來看異步task轉換 TtlRunnable.get(task2) 核心代碼實作,代碼整體實作相對比較簡單,get方法是一個靜态工廠方法,主要作用是将業務傳入的普通Runnable task裝飾成TtlRunable類,并在TtlRunable構造方法中進行線程capture動作(具體實作我們後面再分析),然後将結果存儲到對象屬性capturedRef中。

@Nullable
    public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
        if (null == runnable) return null;

        if (runnable instanceof TtlEnhanced) {
            // avoid redundant decoration, and ensure idempotency
            if (idempotent) return (TtlRunnable) runnable;
            else throw new IllegalStateException("Already TtlRunnable!");
        }
        //将入參runnable進行了裝飾
        return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
    }
  
//......
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
    private final AtomicReference<Object> capturedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.capturedRef = new AtomicReference<Object>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        final Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        final Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
            restore(backup);
        }
    } 

  //........   
 }      

然後是run方法,這也是核心關鍵的CRR操作了。這裡通過模闆方法将CRR操作編排在業務邏輯執行的前後了,也即業務邏輯執行前會将capturer的值進行replay恢複,執行後進行複原restore操作。同樣這裡也有幾個問題很值我們思考:

  1. capture操作為什麼需要放到TtlRunnable構造方法中,而不能在run方法中?
  2. 代碼中使用了哪兩個設計模式,使用設計模式的好處是什麼?
  3. 業務執行完之後為什麼還需要restore操作?

接下來,我們再分别對capture、replay、restore方法實作做個一一分析。首先是capture方法,我們可以看到capture操作整體比較簡單,主要是将set操作儲存到holder變量中的值進行周遊并以Snapshot結構進行存儲傳回。

/**
         * Capture all {@link TransmittableThreadLocal} and registered {@link ThreadLocal} values in the current thread.
         *
         * @return the captured {@link TransmittableThreadLocal} values
         * @since 2.3.0
         */
        @NonNull
        public static Object capture() {
            return new Snapshot(captureTtlValues(), captureThreadLocalValues());
        }

        private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
            HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
            for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
                ttl2Value.put(threadLocal, threadLocal.copyValue());
            }
            return ttl2Value;
        }

        private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
            final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>();
            for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) {
                final ThreadLocal<Object> threadLocal = entry.getKey();
                final TtlCopier<Object> copier = entry.getValue();

                threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
            }
            return threadLocal2Value;
        }      

另一個captureThreadLocalValues,主要是用于将一些已有ThreadLocal中的上下文一起複制,已有ThreadLocal需要通過registerThreadLocal方法來單獨注冊。相關代碼如下:

public static class Transmitter {
    //....
    
  private static volatile WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>> threadLocalHolder = new WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>>();
  private static final Object threadLocalHolderUpdateLock = new Object();

    //......
    public static <T> boolean registerThreadLocal(@NonNull ThreadLocal<T> threadLocal, @NonNull TtlCopier<T> copier, boolean force) {
        if (threadLocal instanceof TransmittableThreadLocal) {
            logger.warning("register a TransmittableThreadLocal instance, this is unnecessary!");
            return true;
        }

        synchronized (threadLocalHolderUpdateLock) {
            if (!force && threadLocalHolder.containsKey(threadLocal)) return false;

            WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>> newHolder = new WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>>(threadLocalHolder);
            newHolder.put((ThreadLocal<Object>) threadLocal, (TtlCopier<Object>) copier);
            threadLocalHolder = newHolder;
            return true;
        }
    }
    //......
}      

這裡代碼有個非常關鍵的處理,由于WeakHashMap非線程安全,為了避免并發問題安全加上了synchronized鎖操作。這裡有可以思考下除了synchronized關鍵字還有什麼保障線程安全的方法。另外,實作threadLocal注冊時為已經在鎖塊中了,為什麼還要做new copy重新替換操作,這樣做目的是什麼?大家可以想想看。

最後就是replay和restore方法,整體實作邏輯非常清晰,主要是将captured的值在目前線程ThreadLocal中進行重新指派初始化,以及業務執行後恢複到原來。這裡很佩服作者對不同情況的細緻考慮,不是直接将目前holder中的上下文直接備份,而是與之前已capture的内容比較,将業務後set的上下文進行剔除,以免在恢複restore時出現前後不一緻的情況。

@NonNull
public static Object replay(@NonNull Object captured) {
    final Snapshot capturedSnapshot = (Snapshot) captured;
    return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}

@NonNull
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
    HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>();

    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocal<Object> threadLocal = iterator.next();

        // backup
        backup.put(threadLocal, threadLocal.get());

        // clear the TTL values that is not in captured
        // avoid the extra TTL values after replay when run task
        if (!captured.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // set TTL values to captured
    setTtlValuesTo(captured);

    // call beforeExecute callback
    doExecuteCallback(true);

    return backup;
}

private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
    for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {
        TransmittableThreadLocal<Object> threadLocal = entry.getKey();
        threadLocal.set(entry.getValue());
    }
}

public static void restore(@NonNull Object backup) {
    final Snapshot backupSnapshot = (Snapshot) backup;
    restoreTtlValues(backupSnapshot.ttl2Value);
    restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}

private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
    // call afterExecute callback
    doExecuteCallback(false);

    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocal<Object> threadLocal = iterator.next();

        // clear the TTL values that is not in backup
        // avoid the extra TTL values after restore
        if (!backup.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // restore TTL values
    setTtlValuesTo(backup);
}      

核心代碼分析完之後,再來簡單總結下項目中學習到的知識點:

  1. 對ThreadLocal、InheritableThreadLocal有了更加系統和深入的了解,包括兩者繼承關系、底層資料結構ThreadLocalMap與Thread關聯關系等。
  2. 面向gc程式設計(gc相關)、WeakHashMap(Java對象引用類型強、軟、弱等)、線程安全、并發等等
  3. 設計模式相關,裝飾模式、工廠、模闆方法、代理等
  4. TTL雖然代碼量不算多,但短小精悍,也處處展現了作者超高的設計和程式設計能力,每行代碼都值得學習和反複琢磨。

我相信通過類似這樣的一個項目學習流程下來,把每個環節都能踏踏實實做好,且過程中有貫穿自己思考和了解。相信你一定能把每個項目吃透,并把項目中的每個技術點都牢牢掌握。

最後,我所在團隊是淘系技術部淘系架構團隊,主要在負責一站式serverless研發平台建設,為業務不斷提升研發效率和極緻體驗。平台已平穩支撐淘系互動、淘寶人生、金币莊園、特價版、閑魚、拍賣、品牌輕店等多個業務的6.18、雙11、雙12、春晚等多個大促活動。