存模型(JMM)入門——可見性、有序性、原子性)
1. 項(xiàng)目背景業(yè)務(wù)場景某社交平臺(tái)的在線狀態(tài)模塊有一個(gè)簡單的設(shè)計(jì)——用boolean isOnline false;標(biāo)志位標(biāo)記用戶登錄狀態(tài)。用戶登錄后主線程設(shè)isOnline true后臺(tái)心跳線程循環(huán)檢查這個(gè)標(biāo)志位。奇怪的是在某些高負(fù)載的機(jī)器上心跳線程看不到isOnline被設(shè)為true——即使登錄成功了用戶狀態(tài)一直顯示離線。開發(fā)反復(fù)檢查代碼邏輯確認(rèn)沒有 bug懷疑是 JVM 的 bug。痛點(diǎn)可見性盲區(qū)Java 內(nèi)存模型JMM規(guī)定不同線程之間對(duì)共享變量的修改不保證立即可見——一個(gè)線程寫入了值另一個(gè)線程可能永遠(yuǎn)讀不到。這不是 bug而是 JMM 允許編譯器、JIT 和 CPU 為了性能做的優(yōu)化寄存器緩存、CPU Store Buffer、指令重排等。指令重排的反直覺開發(fā)寫了a 1; b 2;CPU 可能實(shí)際執(zhí)行的順序是b 2; a 1;。在單線程下毫無影響但在多線程下另一個(gè)線程可能看到b 2時(shí)a還是舊值——這就是有序性被破壞的經(jīng)典案例。volatile的誤用很多人知道volatile能解決可見性但對(duì)它的第二個(gè)作用——“禁止指令重排”——知之甚少。以為給所有字段都加volatile就萬事大吉結(jié)果性能一塌糊涂。本章聚焦 JMM 的三個(gè)核心性質(zhì)——可見性、有序性、原子性——通過編寫刻意暴露問題的代碼讓你親眼看到無同步下的寫丟失“指令重排導(dǎo)致的怪異行為”再用volatile、鎖和AtomicInteger逐個(gè)修復(fù)。2. 項(xiàng)目設(shè)計(jì)小胖瞪著自己的代碼——isOnline true明明在第一行就執(zhí)行了心跳線程卻永遠(yuǎn)讀不到。小胖大師這不可能啊我isOnline true就寫在心跳線程啟動(dòng)之前怎么可能它讀不到這不科學(xué)難道 Java 還能把我的賦值語句給吞了大師淡定地喝了口咖啡Java 沒有吞你的賦值語句——是你的心跳線程的 CPU 核心在吞。來我給你畫一張圖看完你就懂了。┌──────────────────────┐ ┌──────────────────────┐ │ CPU Core 1 │ │ CPU Core 2 │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ 寄存器 │ │ │ │ 寄存器 │ │ │ │ isOnlinetrue│ │ │ │ isOnline??? │ │ │ └──────┬───────┘ │ │ └──────┬───────┘ │ │ │ │ │ │ │ │ ┌──────▼───────┐ │ │ ┌──────▼───────┐ │ │ │ L1/L2 Cache │ │ │ │ L1/L2 Cache │ │ │ │ isOnlinetrue│ │ │ │ isOnline? │ ← 可能還是舊值 │ └──────┬───────┘ │ │ └──────┬───────┘ │ │ │ │ │ │ │ └─────────┼────────────┘ └─────────┼────────────┘ │ │ ┌────▼───────────────────────────────▼───┐ │ 共享內(nèi)存 (Main Memory) │ │ isOnline ???? │ └────────────────────────────────────────┘現(xiàn)代 CPU 為了性能每個(gè)核心有自己的 L1/L2 緩存和 Store Buffer。Core 1 寫入isOnline true后這個(gè)值可能暫時(shí)待在 Core 1 的 L1 緩存里沒有立刻同步到主內(nèi)存。與此同時(shí)Core 2 上的心跳線程讀取的是自己 L1 緩存里的舊值——于是它看不到更新。技術(shù)映射CPU 核心的私有緩存 ? 每個(gè)員工手里的小本本寫了他們各自認(rèn)為的庫存數(shù)量主內(nèi)存 ? 倉庫門口的大公告牌。小本本上的數(shù)字改了但沒更新公告牌——另一個(gè)員工路過公告牌看到的還是舊數(shù)字。小白那volatile就是強(qiáng)制更新公告牌的手段咯大師沒錯(cuò)但volatile做的遠(yuǎn)不止更新公告牌。它實(shí)際上做了兩件事保證可見性對(duì)volatile變量的寫操作會(huì)立即刷新到主內(nèi)存讀操作總是從主內(nèi)存讀。在 x86 架構(gòu)上這對(duì)應(yīng)一條lock前綴的匯編指令——它會(huì)刷寫 Store Buffer寫屏障并使其他核心的 L1 緩存失效讀屏障。禁止指令重排volatile變量的讀寫前后會(huì)插入內(nèi)存屏障Memory Barrier阻止編譯器和 CPU 把volatile寫之前的指令重排到之后以及把volatile讀之后的指令重排到之前。// 經(jīng)典的雙重檢查鎖定 (DCL) 單例publicclassSingleton{privatestaticvolatileSingletoninstance;// ← 必須有 volatilepublicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();// ← 這行代碼分三步執(zhí)行// 1. 分配內(nèi)存// 2. 初始化對(duì)象 (調(diào)用構(gòu)造函數(shù))// 3. 將引用賦值給 instance}}}returninstance;}}如果沒有volatile步驟 2 和步驟 3 可能被重排——另一個(gè)線程在步驟 3 之后、步驟 2 之前讀到instance ! null拿到的是一個(gè)半初始化的對(duì)象。這個(gè) bug 極其隱蔽只在特定 CPU 架構(gòu)和運(yùn)行條件下出現(xiàn)可以在線上潛伏幾個(gè)月。技術(shù)映射volatile? 公告牌警衛(wèi)。更新公告牌時(shí)警衛(wèi)確保之前所有相關(guān)的修改先上牌讀取公告牌時(shí)警衛(wèi)確保之后的操作看到的是最新內(nèi)容。小胖那volatile能解決所有并發(fā)問題嗎我是不是可以把synchronized全部換成volatile性能還好大師果斷地不能。volatile只解決可見性和有序性不能解決原子性。看這個(gè)例子volatileintcount0;// 線程 A 線程 Bcount;// 三步驟 count; // 三步驟count雖然只有一行代碼但在字節(jié)碼層面是三條指令getfield讀→iadd加→putfield寫。如果線程 A 讀完值0在還沒來得及寫回值1時(shí)線程 B 也讀了值0然后兩個(gè)線程各寫回 1——兩個(gè)操作結(jié)果只加了 1。這就是原子性被破壞。volatile保證不了讀-改-寫的原子性。要解決原子性有三個(gè)選擇synchronized重量級(jí)但保證互斥訪問。AtomicIntegerCAS輕量級(jí)適合簡單計(jì)數(shù)。LongAdder適合極高并發(fā)計(jì)數(shù)。技術(shù)映射volatile? 公告牌保證你看到的是最新庫存但如果兩個(gè)人同時(shí)看到 10 個(gè) → 各拿 1 個(gè) → 都更新為 9實(shí)際庫存該是 8 而不是 9。要解決這個(gè)問題你需要在拿貨時(shí)加一把鎖或者用原子操作的自動(dòng)售貨機(jī)。小白那 JMM 中的happens-before規(guī)則又是什么它跟volatile有什么關(guān)系大師在白板上寫下幾條核心規(guī)則happens-before是 JMM 對(duì) Java 程序員提供的秩序保證——只要你的代碼滿足某條happens-before規(guī)則JVM 就保證前一個(gè)操作的結(jié)果對(duì)后一個(gè)操作可見。核心規(guī)則包括程序順序規(guī)則同一個(gè)線程中前面的操作 happens-before 后面的操作但僅限單線程volatile 變量規(guī)則對(duì)一個(gè)volatile變量的寫 happens-before 后續(xù)對(duì)這個(gè)變量的讀鎖規(guī)則一個(gè)鎖的unlockhappens-before 后續(xù)的lock傳遞性如果 A hb BB hb C則 A hb C線程 start/join 規(guī)則thread.start()hb 線程內(nèi)的任何操作線程內(nèi)的任何操作 hbthread.join()返回happens-before是你寫并發(fā)代碼的安全網(wǎng)——只要你能證明代碼滿足其中一條規(guī)則JVM 就保證不亂序。技術(shù)映射happens-before? 火車的軌道調(diào)度系統(tǒng)。如果沒有調(diào)度兩列火車可能相撞并發(fā) bug。有了調(diào)度你只需要按照調(diào)度邏輯開車——調(diào)度系統(tǒng)保障了次序。3. 項(xiàng)目實(shí)戰(zhàn)3.1 環(huán)境準(zhǔn)備組件版本用途JDKOpenJDK 21運(yùn)行示例JMH可選微基準(zhǔn)測(cè)試hsdis可選JDK 自帶反匯編查看內(nèi)存屏障指令3.2 分步實(shí)現(xiàn)步驟一復(fù)現(xiàn)可見性失敗目標(biāo)證明沒有volatile時(shí)一個(gè)線程的寫入對(duì)另一個(gè)線程可能永遠(yuǎn)不可見。// VisibilityDemo.java —— 可見性失敗復(fù)現(xiàn)publicclassVisibilityDemo{// 注意沒有 volatile 修飾privatestaticbooleanflagfalse;privatestaticintvalue0;publicstaticvoidmain(String[]args)throwsInterruptedException{System.out.println(開始演示可見性問題可能需要幾秒到幾分鐘...);// 寫線程修改 flag 和 valueThreadwriternewThread(()-{value42;// 步驟 1flagtrue;// 步驟 2 沒有 volatileCPU 可能重排 1?2},Writer);// 讀線程不停檢查 flag期望讀到 value42ThreadreadernewThread(()-{intiterations0;while(!flag){// ← 沒有 volatile可能永遠(yuǎn)讀不到 trueiterations;// 注意此處不能有 println 或 sleep它們隱含 synchronized會(huì)刷緩存}System.out.println(Reader 在 iterations 次循環(huán)后讀到: flagflag, valuevalue);// 如果 value ! 42說明發(fā)生了指令重排或可見性問題if(value!42){System.out.println(ERROR: valuevalue 但預(yù)期42 (重排或可見性失敗));}},Reader);reader.start();Thread.sleep(100);// 確保 reader 先進(jìn)入循環(huán)writer.start();reader.join(5000);// 最多等 5 秒if(reader.isAlive()){System.out.println(Reader 在 5 秒內(nèi)沒讀到 flagtrue —— 可見性問題確認(rèn));reader.interrupt();}}}運(yùn)行結(jié)果Reader 可能在 5 秒超時(shí)后仍未讀到flagtrue。步驟二用 volatile 修復(fù)可見性目標(biāo)給flag加volatile驗(yàn)證寫入立即對(duì)讀線程可見。// VisibilityFixed.java —— volatile 修復(fù)可見性publicclassVisibilityFixed{privatestaticvolatilebooleanflagfalse;// ← 加 volatileprivatestaticintvalue0;publicstaticvoidmain(String[]args)throwsInterruptedException{System.out.println(使用 volatile寫入應(yīng)立即可見...);ThreadreadernewThread(()-{intiterations0;while(!flag){iterations;}System.out.println(Reader 在 iterations 次后讀到 flagtrue);System.out.println(valuevalue (期望42)(value42? OK: FAIL));},Reader);ThreadwriternewThread(()-{value42;// volatile 寫之前的普通寫也對(duì) reader 可見flagtrue;// volatile 寫 → 觸發(fā) happens-before},Writer);reader.start();Thread.sleep(100);writer.start();reader.join(1000);System.out.println(Reader 是否結(jié)束: !reader.isAlive());}}關(guān)鍵點(diǎn)volatile寫flagtrue不僅保證flag的可見性還保證value42在 volatile 寫之前的所有操作也對(duì)讀線程可見——這是 happens-before 的傳遞性 volatile 變量規(guī)則的聯(lián)合效果。步驟三展示原子性缺失——volatile 的 count 陷阱目標(biāo)證明volatile 不是原子操作。// AtomicityDemo.java —— volatile 無法保證原子性importjava.util.concurrent.CountDownLatch;publicclassAtomicityDemo{privatestaticvolatileintcount0;// volatile, 但不保證原子性privatestaticintcountSync0;// synchronized 保護(hù)privatestaticjava.util.concurrent.atomic.AtomicIntegercountAtomicnewjava.util.concurrent.atomic.AtomicInteger(0);// AtomicIntegerprivatestaticfinalintTHREADS10;privatestaticfinalintITERATIONS10000;publicstaticvoidmain(String[]args)throwsInterruptedException{// 方案 A: volatile 預(yù)期結(jié)果 THREADS * ITERATIONStest(volatile only,()-count);System.out.println(volatile 最終值: count (預(yù)期 THREADS*ITERATIONS, 丟失 (THREADS*ITERATIONS-count)));// 方案 B: synchronized 預(yù)期結(jié)果 THREADS * ITERATIONStest(synchronized,()-{synchronized(AtomicityDemo.class){countSync;}});System.out.println(synchronized 最終值: countSync (預(yù)期 THREADS*ITERATIONS));// 方案 C: AtomicInteger 預(yù)期結(jié)果 THREADS * ITERATIONStest(AtomicInteger,()-countAtomic.incrementAndGet());System.out.println(AtomicInteger 最終值: countAtomic.get() (預(yù)期 THREADS*ITERATIONS));}staticvoidtest(Stringname,Runnabletask)throwsInterruptedException{CountDownLatchlatchnewCountDownLatch(THREADS);for(inti0;iTHREADS;i){newThread(()-{for(intj0;jITERATIONS;j){task.run();}latch.countDown();}).start();}latch.await();System.out.print(name - );}}預(yù)期輸出volatile only - volatile 最終值: 87345 (預(yù)期 100000, 丟失 12655) synchronized - synchronized 最終值: 100000 (預(yù)期 100000) AtomicInteger - AtomicInteger 最終值: 100000 (預(yù)期 100000)步驟四展示 volatile 的內(nèi)存屏障——反匯編驗(yàn)證# 用 hsdis 反匯編查看 volatile 變量的讀寫指令java-XX:UnlockDiagnosticVMOptions\-XX:PrintAssembly\-XX:CompileCommandcompileonly,*VisibilityFixed.*\VisibilityFixed21|grep-A5lock如果能看到lock addl或lock cmpxchg等帶lock前綴的指令這些就是 volatile 的寫屏障。可能遇到的坑可見性實(shí)驗(yàn)總是成功如果在while (!flag)循環(huán)中調(diào)用了System.out.println()或Thread.sleep()這些方法內(nèi)部有synchronized塊——它會(huì)隱式地刷新 CPU 緩存導(dǎo)致自然可見。所以可見性實(shí)驗(yàn)的循環(huán)體中必須不能有任何同步操作。JIT 的死循環(huán)優(yōu)化如果 JIT 發(fā)現(xiàn)while (!flag)中的flag不是volatile它可能直接把flag的值緩存在寄存器里并優(yōu)化成一個(gè)無限循環(huán)while(true)——這確實(shí)會(huì)發(fā)生。這就是為什么有時(shí)候需要在flag上額外做一個(gè)空殼操作來抑制優(yōu)化。javac編譯優(yōu)化如果flag是false且從未修改的字面量常量編譯器可能直接優(yōu)化掉整個(gè)while循環(huán)——確保flag在運(yùn)行時(shí)可能被修改。不同 CPU 架構(gòu)差異x86 是強(qiáng)內(nèi)存模型TSO很多重排在 x86 上不會(huì)發(fā)生但 ARM 和 RISC-V 是弱內(nèi)存模型——在 x86 上能跑通的并發(fā)代碼在 ARM Mac 上可能立刻暴露 bug。跨平臺(tái)驗(yàn)證很重要。3.3 測(cè)試驗(yàn)證驗(yàn)證點(diǎn)方法預(yù)期結(jié)果可見性失敗運(yùn)行 VisibilityDemoReader 5 秒內(nèi)讀不到 flagtruevolatile 修復(fù)運(yùn)行 VisibilityFixedReader 立即可見value42原子性失敗運(yùn)行 AtomicityDemovolatile count 遠(yuǎn)小于 100000happens-before線程 start/join 驗(yàn)證join 后的線程操作必然可見指令重排效果多次運(yùn)行無 volatile 的 VisibilityDemovalue 可能不是 42#!/bin/bashecho 1. 可見性失敗演示 javaVisibilityDemoechoecho 2. volatile 修復(fù)驗(yàn)證 javaVisibilityFixedechoecho 3. 原子性驗(yàn)證 javaAtomicityDemoechoecho 4. volatile 傳遞性驗(yàn)證 (happens-before) # 編譯并運(yùn)行額外的驗(yàn)證volatile 寫之前的非 volatile 寫是否可見java-cp.VisibilityFixed# value42 在 volatile 寫 flagtrue 之前 → 應(yīng)對(duì) reader 可見4. 項(xiàng)目總結(jié)4.1 優(yōu)點(diǎn)與缺點(diǎn)維度優(yōu)點(diǎn)缺點(diǎn)volatile輕量級(jí)讀操作等同于普通讀保證可見性有序性不保證原子性——讀-改-寫需要額外同步happens-before提供清晰的并發(fā)正確性判斷框架規(guī)則較多8 條核心規(guī)則需要刻意記憶JMM 設(shè)計(jì)平衡了性能與安全性——不強(qiáng)求順序一致性的所有開銷理解門檻高——需要同時(shí)理解編譯器優(yōu)化 CPU 緩存 指令重排Atomic* 類無鎖 CAS 操作極高并發(fā)下吞吐遠(yuǎn)超鎖CAS 失敗時(shí)自旋消耗 CPUABA 問題需要額外關(guān)注強(qiáng)/弱內(nèi)存模型x86 的 TSO 讓很多并發(fā) bug 被隱藏——部署到 ARM 前可幸免在 x86 上測(cè)試通過的代碼不等于正確——必須有意識(shí)地驗(yàn)證可見性和有序性對(duì)比技術(shù)volatilesynchronizedAtomicInteger (CAS)互斥性不支持支持不支持僅單變量原子操作性能讀 ≈ 普通讀寫略慢競爭時(shí)最慢無競爭時(shí)極快高競爭時(shí)自旋浪費(fèi)適用操作單變量讀寫任意代碼塊單變量的增減/CAS內(nèi)存屏障讀寫各一屏障進(jìn)出各一屏障一次 CAS 讀寫屏障4.2 適用場景狀態(tài)標(biāo)志位如boolean shutdown、boolean initialized——一個(gè)線程寫、多個(gè)線程讀用volatile完美解決。DCL 單例雙重檢查鎖定中的instance必須volatile防止半初始化對(duì)象泄漏。無鎖計(jì)數(shù)器AtomicLong、LongAdder在高并發(fā)計(jì)數(shù)場景下秒殺任何鎖方案。輕量級(jí)讀-寫鎖用volatile修飾狀態(tài)變量配合 CAS 實(shí)現(xiàn)簡易讀寫分離。并發(fā)框架基礎(chǔ)理解 AQS、ConcurrentHashMap等高級(jí)并發(fā)工具必須先理解 JMM。不適用場景讀-改-寫的復(fù)合操作——volatile 不能保證原子性應(yīng)選用Atomic*或鎖。多個(gè)變量需要保持一致性——比如A 和 B 必須同時(shí)更新volatile 無法保證兩個(gè)變量更新的原子性。單線程場景——沒有可見性問題不加任何修飾即可。4.3 注意事項(xiàng)類型詳細(xì)說明volatile 數(shù)組volatile int[] arr只保證數(shù)組引用本身的可見性不保證數(shù)組元素arr[i]的可見性——用AtomicIntegerArray替代64 位變量long和double的 64 位操作在 JMM 下被分為兩次 32 位讀寫——非 volatile 的long可能讀到半個(gè)舊值半個(gè)新值final 域安全發(fā)布構(gòu)造函數(shù)中對(duì)final字段的寫入 happens-before 構(gòu)造函數(shù)返回——這是 JMM 提供的對(duì)象安全發(fā)布保障前提是不能讓this在構(gòu)造過程中逃逸VarHandleJDK 9 引入的VarHandle提供了比volatile更細(xì)粒度的內(nèi)存順序控制——支持getAcquire/setRelease/getOpaque/setOpaque四種模式4.4 常見踩坑經(jīng)驗(yàn)案例 1雙重檢查鎖定的半初始化炸彈某核心服務(wù)的配置管理器用 DCL 單例緩存配置。JDK 7 下運(yùn)行了 3 年從未出錯(cuò)。遷移到 JDK 11 ARM 服務(wù)器后偶發(fā)性地讀出config.get(key)返回null配置文件確定有這個(gè) key。根因單例的instance字段沒加volatileARM 的弱內(nèi)存模型下指令重排導(dǎo)致分配內(nèi)存 → 引用賦值 → 初始化的順序被執(zhí)行——?jiǎng)e的工作線程看到了非空但未初始化的 instance。修復(fù)加volatile——private static volatile Config instance;。案例 2volatile的傳遞性誤解某交易系統(tǒng)用volatile boolean orderSubmitted false;標(biāo)志訂單提交狀態(tài)。提交線程順序?qū)憃rder.setAmount(100);→orderSubmitted true;。處理線程讀到orderSubmitted true后立刻讀order.getAmount()期望拿到 100。結(jié)果在 ARM Mac 上測(cè)試時(shí)偶爾拿到 0。根因開發(fā)誤以為 volatile 只有立即可見不了解它的傳遞序——orderSubmitted truevolatile 寫確實(shí)讓后續(xù) volatile 讀可見但后續(xù) volatile 讀之前發(fā)生了什么volatile 不保證正確順序是orderSubmittedvolatile 寫在order.setAmount之前。案例 3System.exit(0)觸發(fā) DCL 失敗垃圾回收線程在 JVM 關(guān)閉前的最后一瞬間觸發(fā)了finalize()調(diào)用了單例的getInstance()——此時(shí)構(gòu)造函數(shù)剛好執(zhí)行到一半還沒退出synchronized塊得到半初始化對(duì)象。根因System.exit()的特殊性與 DCL 交互的極端邊緣條件。修復(fù)使用基于類的初始化Class Initialization Lock替代 DCLprivatestaticclassHolder{staticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}4.5 思考題進(jìn)階題volatile修飾的long變量在多線程中執(zhí)行i是否線程安全如果不安全請(qǐng)分析字節(jié)碼層面涉及了多少條指令并解釋AtomicLong.incrementAndGet()的內(nèi)部實(shí)現(xiàn)原理提示查看Unsafe.compareAndSwapLong的 native 實(shí)現(xiàn)。實(shí)戰(zhàn)題你的系統(tǒng)從 x86 遷移到 ARM 服務(wù)器如 AWS Graviton后之前跑了 2 年的無 bug并發(fā)代碼突然出現(xiàn)了間歇性的數(shù)據(jù)不一致。懷疑是指令重排導(dǎo)致。請(qǐng)?jiān)O(shè)計(jì)一個(gè)最小可復(fù)現(xiàn)方案不依賴任何外部工具并給出修復(fù)建議。答案提示思考題 1 答案見本章步驟三 第 17 章 AQS/CAS 部分思考題 2 答案見第 23 章逃逸分析與鎖優(yōu)化。下一章預(yù)告第 9 章將進(jìn)入 GC 的世界——堆分代模型、Serial/Parallel 垃圾收集器以及如何用 Unified Logging 繪制分配速率-停頓曲線。延伸閱讀與資源Redis 8 實(shí)戰(zhàn)精講從 CRUD 到源碼構(gòu)建高可用緩存系統(tǒng)Redis 實(shí)戰(zhàn)修煉與原理進(jìn)階Python 3實(shí)戰(zhàn)精進(jìn)從腳本到高并發(fā)訂單引擎MongoDB 實(shí)戰(zhàn)進(jìn)階與內(nèi)核修煉python入門Rquests從菜鳥腳本到企業(yè)級(jí)SDK的網(wǎng)絡(luò)實(shí)戰(zhàn)圣經(jīng)Milvus向量數(shù)據(jù)庫實(shí)戰(zhàn)修煉從 0 到 1精通向量檢索與生產(chǎn)落地后端工程師的 AI 轉(zhuǎn)型第一課Ollama 與私有化大模型實(shí)戰(zhàn)10倍開發(fā)者的 Dify 魔法書從零構(gòu)建全棧 AI 應(yīng)用后端工程師轉(zhuǎn)型AI第一課-Ollama 與私有化大模型實(shí)戰(zhàn)大型語言模型(LLM) vLLM 高性能推理落地實(shí)戰(zhàn)Agent開發(fā)之LlamaIndex 實(shí)戰(zhàn)修煉與源碼進(jìn)階大語言模型Transformers 實(shí)戰(zhàn)修煉與源碼剖析