【JVM】- 內存模式

Java內存模型:JMM(Java Memory Model),定義了一套在多線程環境下,讀寫共享數據(成員變量、數組)時,對數據的可見性,有序性和原子性的規則和保障。


原子性

問題分析

問題】:兩個線程對初始值為0的靜態變量操作,一個線程做自增,一個線程做自減,各做50000次,結果是0嗎?

public class Demo01 {static int i = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int j = 0; j < 50000; ++j) {i++;}});Thread t2 = new Thread(() -> {for (int j = 0; j < 50000; ++j) {--i;}});t1.start();t2.start();// 讓主線程等待t1和t2兩個子線程執行完畢后,再執行后續代碼t1.join();t2.join();System.out.println(i);}
}

結果】:上邊代碼輸出,每次運行的結果不一樣。

原因】:Java中對靜態變量的自增自減并不是原子操作,對于i++而言:

getstatic i // 獲取靜態變量i的值
iconst_1 // 準備常量1
iadd // 加法
putstatic i // 將修改后的值存入靜態變量i中

Java的內存模型如下,如果需要完成靜態變量的自增、自減,需要在主內存和工作線程的內存中進行交換數據。
在這里插入圖片描述

由于當線程是按順序執行,所以并不會出現問題。
但是在多線程下,可能出現交錯運行。線程是一個搶占式的,大家都是輪流使用CPU

解決方法

synchronized(對象) {要作為原子操作的代碼
}

修正后:

public class Demo02 {static int i = 0;static Object obj = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (obj) {for (int j = 0; j < 50000; ++j) {i++;}}});Thread t2 = new Thread(() -> {synchronized (obj) {for (int j = 0; j < 50000; ++j) {--i;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}

這樣++i和–i的四條指令都可以作為一個整體來運行。
并且:t1和t2必須鎖的是同一個obj對象(相當于這兩個人進入了兩個不同的房間)

Monitor(監視器)

Monitor 是一種線程同步機制,可以理解為 對象鎖 的內部實現。每個 Java 對象(Object)在 JVM 內部都有一個關聯的 Monitor,用于實現 synchronized 同步機制。

Monitor 的組成部分

(1) Owner(持有者)

  • 作用:表示當前持有 Monitor 的線程。
  • 特點
    • 當線程進入 synchronized 代碼塊時,會嘗試獲取 Monitor 的 owner 權限。
    • 如果 ownernull(即沒有線程持有鎖),當前線程會成為 owner
    • 如果 owner 已經被其他線程持有,當前線程會進入 entryList 等待。

(2) EntryList(入口隊列)

  • 作用:存儲 競爭鎖的線程(即等待獲取鎖的線程)。
  • 特點
    • 當線程 A 持有鎖時,線程 B 嘗試進入 synchronized 代碼塊,會進入 entryList 并進入 BLOCKED 狀態。
    • 當線程 A 釋放鎖(退出 synchronized 代碼塊),JVM 會從 entryList 中喚醒一個線程,使其競爭鎖。

(3) WaitSet(等待隊列)

  • 作用:存儲 調用了 wait() 的線程(即主動放棄鎖的線程)。
  • 特點
    • 當線程 A 調用 wait() 時,它會釋放鎖,并進入 waitSet,狀態變為 WAITING
    • 當其他線程調用 notify()notifyAll() 時,JVM 會從 waitSet 中隨機喚醒一個(或全部)線程,使其重新競爭鎖。

3. Monitor 的工作流程

synchronized (obj) {  // 1. 嘗試獲取 Monitor 的 ownerwhile (!condition) {obj.wait();    // 2. 釋放鎖,進入 waitSet}// 3. 執行同步代碼
}
obj.notify();          // 4. 喚醒 waitSet 中的線程
  1. 線程 A 進入 synchronized 代碼塊
    • 檢查 owner,如果為空,線程 A 成為 owner
    • 如果 owner 已被線程 B 持有,線程 A 進入 entryList(BLOCKED 狀態)。
  2. 線程 A 調用 wait()
    • 釋放 owner,線程 A 進入 waitSet(WAITING 狀態)。
    • JVM 從 entryList 中喚醒一個線程(如線程 B),使其成為新的 owner
  3. 線程 B 調用 notify()
    • waitSet 中隨機喚醒一個線程(如線程 A),使其重新進入 entryList(BLOCKED 狀態)。
    • 線程 A 需要重新競爭鎖(不會立即獲得鎖)。
  4. 線程 B 退出 synchronized 代碼塊
    • 釋放 owner,JVM 從 entryList 中選擇一個線程(如線程 A),使其成為新的 owner

可見性

問題分析

問題】:main線程對于run變量的修改對t線程是不可見的,這就導致了t線程無法停止:

public class Demo03 {static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (run) {// ...}});t.start();Thread.sleep(1000);run = false; // 不會停下來}
}

原因】:

  1. 初始的時候,t線程剛開始從main線程的內存中讀取了run的值到工作線程
  2. 因為t線程要頻繁的從主內存中讀取run的值,JIT編譯器會將run的值緩存到自己的工作內存中的高速緩沖區中,這樣就可以減少對主內存的讀取。
  3. 主線程睡眠1s后,main線程修改了run的值,并同步到貯存,而t線程仍然是從自己工作內存中的高速緩存中讀取這個變量的值,結果永遠是舊值。
    在這里插入圖片描述

解決辦法

volatile(易變關鍵字):用來修飾成員變量和靜態成員變量,可以避免線程從自己的工作緩存中查找變量的值必須到主存中獲取它的值,線程操作volatile變量都是直接操作主內存。

public class Demo03 {static volatile boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (run) {// ...}});t.start();Thread.sleep(1000);run = false; // 不會停下來}
}
  • volatile:保證的是在多個線程之間,一個線程對volatile變量的修改對另一個線程可見,但是不能保證原子性,只能用在一個寫線程,多個讀線程的情況。
  • synchronized:既可以保證原子性,也能保證代碼塊變量的可見性。但是缺點是:synchronized屬于重量級操作,性能相對更低。

補充】:在上邊的代碼中,如果不加volatile,但是在for循環里加System.out.println(),t線程也能正常看到對run變量的修改。
原因】:System.out.println()底層使用syncronized關鍵字,強制要求當前的線程不要從高速緩存中獲取,從主線程中獲取。

有序性

問題分析——指令重排序

public class Demo04 {static int num = 0;static boolean ready = false;// 線程1:執行此方法public static void actor1(I_Result r) {if(ready) {r.r1 = num + num;}else {r.r1 = 1;}}// 線程2:執行此方法public static void actor2(I_Result r) {num = 2;ready = true;}public static void main(String[] args) throws InterruptedException {I_Result r = new I_Result();Thread t1 = new Thread(() -> {actor1(r);});Thread t2 = new Thread(() -> {actor2(r);});t1.start();t2.start();t1.join();t2.join();System.out.println(r.r1);}
}class I_Result {int r1;
}

上邊的代碼執行一共可能會有三種不同的輸出:

  1. 正常執行:t2先執行完,t1后執行 ==> 輸出4
  2. t1先執行完,t2后執行 ==> 輸出1
  3. 指令重排序(導致ready先變成true,num還未賦值)
    • t2先執行ready = true
    • t1執行(此時num還未被t2修改):r.r1 = num + num = 0
    • t2再執行num = 2(但是t1已經計算完畢,不會影響結果)

解決方法

如果要保證線程安全,可以:

  1. 使用 volatile 修飾 readynum,禁止指令重排序,并保證可見性:
   static volatile int num = 0;static volatile boolean ready = false;
  • 這樣 t2num = 2ready = true 不會重排序,且 t1 能立即看到修改。
  • 可能的輸出:14(不會出現 0)。
  1. 使用 synchronized 加鎖,確保原子性:
   public static synchronized void actor1(I_Result r) { ... }public static synchronized void actor2(I_Result r) { ... }
  • 這樣 t1t2 不會同時執行,輸出一定是 14

有序性理解

static int i, j;
// 在某個線程內執行:
i = ...; // 較為耗時的操作
j = ...; 

由于這段代碼先執行i還是先執行j對結果并不會有影響,所以上面代碼的執行可以是先對i賦值,再對j賦值;也可以是先對j賦值,再對i賦值

案例:雙重檢查鎖

public class Singleton {private Singleton(){}private static Singleton INSTANCE = null;public static Singleton getInstance(){// 實例沒創建,才會進入內部的synchronized代碼塊if(INSTANCE == null){synchronized (Singleton.class){// 也許有其他線程已經創建實例,所以再判讀一次if(INSTANCE == null){INSTANCE = new Singleton();}}}return INSTANCE;}
}

上邊是通過懶漢式的方式實現單例模式,只有首次使用getInstance()才使用synchronized加鎖,后續使用無需加鎖。

多線程下可能的問題

但是在多線程環境下,上邊的代碼是有問題的
INSTANCE = new Singleton();這行代碼在JVM中并不是原子操作,它分為三個步驟:

  1. 分配內存空間(malloc)
  2. 初始化對象(調用構造方法Sington())
  3. 將INSTANCE指向分配的內存地址(賦值)

但是JVM可能會對指令重排序(優化執行順序),變成:

  1. 分配內存空間
  2. 將INSTANCE指向分配的內存地址(此時INSTANCE != null,但是對象未初始化)
  3. 初始化對象(調用構造方法)

如果發生這種重排序,可能導致:

  • 線程 A 執行 INSTANCE = new Singleton();,但只完成了 步驟 1 和 2(INSTANCE 已不為 null,但對象未初始化)。
  • 線程 B 調用 getInstance(),發現 INSTANCE != null,直接返回 未初始化完成的對象,導致錯誤!

解決辦法

使用volatile禁止指令重排序

private static volatile Singleton INSTANCE = null;

happens-before

是JMM的核心規則,定義了 多線程環境下操作的可見性和順序性,確保一個線程對共享變量的修改能被其他線程正確觀察到。


Java 內存模型定義了 6 種 Happens-Before 規則:程序順序、鎖、volatile、線程啟動、線程終止、傳遞性

(1) 程序順序規則(Program Order Rule)

在同一個線程中,前面的操作 Happens-Before 后面的操作。

int x = 1;    // (1)
int y = x + 1; // (2) —— (1) Happens-Before (2)
  • 單線程下,代碼順序執行,(1) 的結果對 (2) 可見。

(2) 鎖規則(Monitor Lock Rule)

解鎖操作 Happens-Before 后續的加鎖操作。

synchronized (lock) {x = 10;    // (1)
}              // 解鎖 (1) Happens-Before 后續的加鎖
synchronized (lock) {int y = x; // (2) —— 能讀到 x = 10
}
  • 線程 A 解鎖后,線程 B 加鎖時能看到 A 的修改。

(3) volatile 變量規則(Volatile Variable Rule)

volatile 變量的寫操作 Happens-Before 后續的讀操作。

volatile boolean flag = false;// 線程 A
flag = true;   // (1) —— 寫操作// 線程 B
if (flag) {    // (2) —— (1) Happens-Before (2),能讀到 flag = true// do something
}
  • volatile 保證可見性,寫操作后,讀操作一定能看到最新值。

(4) 線程啟動規則(Thread Start Rule)

線程的 start() 方法 Happens-Before 該線程的所有操作。

int x = 0;Thread t = new Thread(() -> {System.out.println(x); // (2) —— 能讀到 x = 1
});
x = 1;                    // (1)
t.start();                 // (1) Happens-Before (2)
  • 主線程修改 x = 1 后,子線程能讀到這個值。

(5) 線程終止規則(Thread Termination Rule)

線程的所有操作 Happens-Before 它的終止檢測(如 join())。

int x = 0;Thread t = new Thread(() -> {x = 1;                // (1)
});
t.start();
t.join();                // (2) —— (1) Happens-Before (2)
System.out.println(x);    // 輸出 1
  • 子線程修改 x = 1 后,主線程 join() 后能讀到最新值。

(6) 傳遞性規則(Transitivity Rule)

**如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

int x = 0;
volatile boolean flag = false;// 線程 A
x = 1;            // (1)
flag = true;      // (2) —— (1) Happens-Before (2)// 線程 B
if (flag) {       // (3) —— (2) Happens-Before (3)System.out.println(x); // 輸出 1 —— (1) Happens-Before (3)
}
  • 由于 (1) → (2) → (3),所以 (1)(3) 可見。

CAS與原子類

CAS

CAS:Compare and Swap,是一種樂觀鎖的思想。
案例】多個線程要對一個共享變量的整型變量執行 + 1操作:

// 需要不斷嘗試
while(true) {int 舊值 = 共享變量; // 舊值 = 0int 結果 = 舊值 + 1// 結果 = 0 + 1 = 1/*這時候如果別的線程把共享變量改成了5,本線程的正確結果1就作廢了,此時:compareAdnSwap:返回false,重新嘗試,直到:compareAndSwap:返回true,表示本線程做修改的同時,其他線程沒有干擾*/if(compareAndSwap(舊值, 結果)) {// 成功,退出循環}
}

注意】:
共享變量一定要用volatile修飾,保證共享變量的可見性,當前線程拿到的共享變量必須一定要是新值。(結合CAS和volatile就可以實現無鎖并發了,適用于競爭不激烈、多核CPU的場景)

  • 如果競爭激烈,線程重試會頻繁發生,效率會受到影響
  • 因為沒有使用synchronized,線程并不會陷入阻塞,這也是效率提升的因素

CAS底層依賴于Unsafe類來直接調用操作系統底層的CAS指令

樂觀鎖與悲觀鎖

CAS:最樂觀的估計,不怕別的線程來修改共享變量,如果改了就重試即可。
synchronized:最悲觀的估計,得防著其他線程來修改共享變量,直接給代碼上鎖,等執行完解開鎖了,其他線程才有機會執行。

原子操作類

juc(java.util.concurrent)包下提供了原子操作類,可以提供線程安全的操作,例如:AtomicInteger、AtomicBoolean…,他們的底層就是使用CAS + volatile來實現的。

public class Demo05 {static AtomicInteger i = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int j = 0; j < 50000; ++j) {i.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int j = 0; j < 50000; ++j) {i.getAndDecrement();}});t1.start();t2.start();// 讓主線程等待t1和t2兩個子線程執行完畢后,再執行后續代碼t1.join();t2.join();System.out.println(i);}
}

synchronized優化

JVM中,每個對象都有對象頭(包括class指針、Mark Word)。Mark Word平時存儲這個對象的哈希碼、分代年齡…,當加鎖時,這些信息就會被替換成標記位、線程鎖記錄指針、重量級指針、線程id…

輕量級鎖

如果一個對象雖然有多個線程訪問,但多線程訪問的時間是錯開的(沒有競爭),那么可以用輕量級鎖來優化。

這就類似于:學生A(線程A)用課本占座,短暫的離開教室了一下(時間片到)

  • 回來發現課本沒變(沒有競爭),就會繼續上課(仍然保持輕量級鎖)
  • 如果期間又來了一個學生B(線程B),就會告知學生A(線程A)此時有并發訪問,線程A就會升級成重量級鎖,進入重量級鎖的流程。

每個線程的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word。

static Object obj = new Object();
public static void method1() {synchronized(obj) {// 同步塊Amethod2();}
}
public static void method2() {synchronized(obj) {// 同步塊B}
}
線程1對象Mark Word線程2
訪問同步塊A,把MarkWord賦值到線程1的鎖記錄01(無鎖)-
CAS修改MarkWord為線程1鎖記錄01(無鎖)-
成功(加鎖)00(輕量級鎖)線程1鎖記錄地址-
執行同步塊A00(輕量級鎖)線程1鎖記錄地址-
訪問同步塊B,把MarkWord賦值到線程1的鎖記錄00(輕量級鎖)線程1鎖記錄地址-
CAS修改MarkWord為線程1鎖記錄00(輕量級鎖)線程1鎖記錄地址-
失敗(發現是自己的鎖)00(輕量級鎖)線程1鎖記錄地址-
鎖重入00(輕量級鎖)線程1鎖記錄地址-
執行同步塊B00(輕量級鎖)線程1鎖記錄地址-
同步塊B執行完畢00(輕量級鎖)線程1鎖記錄地址-
同步塊A執行完畢00(輕量級鎖)線程1鎖記錄地址-
成功(解鎖)01(無鎖)-
-01(無鎖)訪問同步塊A,把MarkWord賦值到線程2的鎖記錄
-01(無鎖)CAS修改MarkWord為線程2鎖記錄
-00(輕量級鎖)線程1鎖記錄地址成功(加鎖)

鎖膨脹

在嘗試加輕量級鎖的過程中,CAS操作無法成功,這時如果其他線程為這個對象加上輕量級鎖(有競爭),這時就需要進行鎖膨脹,將輕量級鎖變為重量級鎖

static Object obj = new Object();
public static void method1() {synchronized(obj) {// 同步塊}
}
線程1對象Mark Word線程2
訪問同步塊,把MarkWord賦值到線程1的鎖記錄01(無鎖)-
CAS修改MarkWord為線程1鎖記錄01(無鎖)-
成功(加鎖)00(輕量級鎖)線程1鎖記錄地址-
執行同步塊00(輕量級鎖)線程1鎖記錄地址-
執行同步塊00(輕量級鎖)線程1鎖記錄地址訪問同步塊,把MarkWord賦值到線程2
執行同步塊00(輕量級鎖)線程1鎖記錄地址CAS修改MarkWord為線程2鎖記錄
執行同步塊00(輕量級鎖)線程1鎖記錄地址失敗(發現別人已經占了鎖)
執行同步塊00(輕量級鎖)線程1鎖記錄地址CAS修改Mark為重量級鎖
執行同步塊10(重量級鎖)重量鎖指針阻塞中
執行完畢10(重量級鎖)重量鎖指針阻塞中
失敗(解鎖)10(重量級鎖)重量鎖指針阻塞中
釋放重量鎖,喚起阻塞線程競爭10(重量級鎖)重量鎖指針阻塞中
-10(重量級鎖)重量鎖指針競爭重量鎖
-10(重量級鎖)重量鎖指針成功(加鎖)

加重量級鎖是為了后邊喚醒的時候,根據重量級鎖的指針喚醒阻塞中的線程。

重量級鎖

重量級鎖競爭時,可以使用自旋來進行優化,如果當時線程自旋成功(說明此時持有鎖的線程已經退出同步代碼塊,釋放鎖),此時當前線程就可以避免阻塞,直接進入運行狀態。

自旋鎖是自適應的

  • 對象剛剛的一次自選操作成功了,那么認為這次自旋成功的可能性會高,就會多自旋幾次;
  • 反之,就少自旋 或 不自旋

注意,自旋會占用CPU時間,只有多核的CPU才能發揮自旋的優勢。

偏向鎖

只有第一次使用CAS將線程ID設置到對象的Mark Word投,之后發現這個線程ID是自己的,就表示沒有競爭,不用重新CAS。

其他優化

  1. 較少上鎖時間:同步代碼塊中盡量短
  2. 減少鎖的粒度:將一個鎖拆分成多個鎖提高并發度
  • ConcurrentHashMap:每次只鎖住了一個部分,其他讀取操作不會受到影響。
  • LongAdder:累加工具類,分為base和cells兩部分
    • 沒有并發爭用或cells數組正在初始化時,就會使用CAS來累加到base
    • 有并發爭用,就會初始化cells數組,數組有多少個cell,就允許多少線程并行修改,最后將數組中每個cell累加,再加上base就是最終的值
  • LinkedBlockingQueue:出隊和入隊使用的就是不同的鎖,相對于LinkedBlockingArray只有一個鎖效率要高
  1. 鎖粗化:StringBuffer的append方法都會調用synchronized來進行同步保護,如果不加以限制,那么下邊這段代碼會重復調用三次synchronized。JVM會將多次的append的加鎖操作粗化為一次(因為都是一個對象加鎖,沒必要重入多次)
new StringBuffer().append("a").append("b").append("c");
  1. 鎖消除:JVM會進行代碼的逃逸分析,例如某個加鎖對象是方法內局部變量,不會被其他線程訪問到,這時就會被即時編譯器忽略掉所有同步操作。
  2. 讀寫分離:CopyOnWriteArrayList、CopyOnWriteSet(讀原始數組的內容;寫操作會復制一份,在新數組上進行寫操作)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/85015.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/85015.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/85015.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AQS獨占模式——資源獲取和釋放源碼分析

AQS資源獲取&#xff08;獨占模式&#xff09; Node節點類 static final class Node {//標記當前節點的線程在共享模式下等待。static final Node SHARED new Node();//標記當前節點的線程在獨占模式下等待。static final Node EXCLUSIVE null;//waitStatus的值&#xff0c…

壓測過程中TPS上不去可能是什么原因

進行性能分析 接口沒有報錯或者錯誤率低于1%&#xff0c;繼續增加并發還是一樣&#xff0c;這個時候需要考慮幾點 1.是否觸發限流&#xff0c;比如waf、Nginx等情況&#xff0c;有沒有一些限流的情況&#xff0c;如果觸發了限流&#xff0c;請求是沒有達到后端的&#xff0c;所…

Golang 解大整數乘法

文章目錄 Golang 解大整數乘法問題描述&#xff1a;LeetCode 43. 字符串相乘思路Golang 代碼 Golang 解大整數乘法 在初學 C 語言的時候&#xff0c;我們一定接觸過“字符串相加”或“字符串相乘”之類的問題&#xff0c;對于初學者而言&#xff0c;這類問題的難度一般來說是比…

web3-區塊鏈的技術安全/經濟安全以及去杠桿螺旋(經濟穩定)

web3-區塊鏈的技術安全/經濟安全以及去杠桿螺旋&#xff08;經濟穩定&#xff09; 三個基本設計問題 技術安全 在技術結構中對其進行原子級的、瞬時利用&#xff08;無風險&#xff09; 無風險&#xff0c;因為攻擊者的結果還是二進制的&#xff1a; 只會是攻擊成功 獲利或…

Java多線程通信:wait/notify與sleep的深度剖析(時序圖詳解)

在Java多線程編程中&#xff0c;線程間的通信與協作是實現復雜并發邏輯的關鍵。wait()、notify()以及sleep()方法作為線程控制的重要工具&#xff0c;有著各自獨特的使用場景與規則。本文將深入探討wait()和notify()的協作機制&#xff0c;以及sleep()的阻塞特性&#xff0c;同…

關于使用EasyExcel、 Vue3實現導入導出功能

后端部分: 其中查詢數據的服務省略 1、引用 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.3</version></dependency> 2、controller package com.rs.cphs.sys.controller;i…

機器學習中的數據準備關鍵技術

有效的數據準備對于構建強大的機器學習模型至關重要。本文檔總結并闡述了為監督和非監督學習任務準備數據的關鍵技術。 1. 理解數據類型 有兩種數據類型。定性數據描述對象的特征&#xff0c;而定量數據描述對象的數量。 定性&#xff08;分類&#xff09;數據 名義&#x…

深度學習——基于卷積神經網絡實現食物圖像分類【3】(保存最優模型)

文章目錄 引言一、項目概述二、環境配置三、數據預處理3.1 數據轉換設置3.2 數據集準備 四、自定義數據集類五、CNN模型架構六、訓練與評估流程6.1 訓練函數6.2 評估與模型保存 七、完整訓練流程八、模型保存與加載8.1 保存模型8.2 加載模型 九、優化建議十、常見問題解決十一、…

《棒球百科》棒球怎么玩·棒球9號位

用最簡單的方式介紹棒球的核心玩法和規則&#xff0c;完全零基礎也能看懂&#xff1a; 一句話目標 進攻方&#xff1a;用球棒把球打飛&#xff0c;然后拼命跑完4個壘包&#xff08;逆時針繞一圈&#xff09;得分。 防守方&#xff1a;想盡辦法讓進攻方出局&#xff0c;阻止他…

語言模型是怎么工作的?通俗版原理解讀!

大模型為什么能聊天、寫代碼、懂醫學&#xff1f; 我們從四個關鍵模塊&#xff0c;一步步拆開講清楚 &#x1f447; ? 模塊一&#xff1a;模型的“本事”從哪來&#xff1f;靠訓練數據 別幻想它有意識&#xff0c;它的能力&#xff0c;全是“喂”出來的&#xff1a; 吃過成千…

nrf52811墨水屏edp_service.c文件學習

on_connect函數 /**brief Function for handling the ref BLE_GAP_EVT_CONNECTED event from the S110 SoftDevice.** param[in] p_epd EPD Service structure.* param[in] p_ble_evt Pointer to the event received from BLE stack.*/ static void on_connect(ble_epd_t …

Nginx-2 詳解處理 Http 請求

Nginx-2 詳解處理 Http 請求 Nginx 作為當今最流行的開源 Web 服務器之一&#xff0c;以其高性能、高穩定性和豐富的功能而聞名。在處理 HTTP請求 的過程中&#xff0c;Nginx 采用了模塊化的設計&#xff0c;將整個請求處理流程劃分為若干個階段&#xff0c;每個階段都可以由特…

40-Oracle 23 ai Bigfile~Smallfile-Basicfile~Securefile矩陣對比

小伙伴們是不是在文件選擇上還默認給建文件4G/個么&#xff0c;在oracle每個版本上系統默認屬性是什么&#xff0c;選擇困難癥了沒&#xff0c;一起一次性文件存儲和默認屬性看透。 基于Oracle歷代在存儲架構的技術演進分析&#xff0c;結合版本升級和23ai新特性&#xff0c;一…

【一】零基礎--分層強化學習概覽

分層強化學習&#xff08;Hierarchical Reinforcement Learning, HRL&#xff09;最早一般視為1993 年封建強化學習的提出. 一、HL的基礎理論 1.1 MDP MDP&#xff08;馬爾可夫決策過程&#xff09;&#xff1a;MDP是一種用于建模序列決策問題的框架&#xff0c;包含狀態&am…

Java延時

在 Java 中實現延時操作主要有以下幾種方式&#xff0c;根據使用場景選擇合適的方法&#xff1a; 1. Thread.sleep()&#xff08;最常用&#xff09; java 復制 下載 try {// 延時 1000 毫秒&#xff08;1秒&#xff09;Thread.sleep(1000); } catch (InterruptedExcepti…

電阻篇---下拉電阻的取值

下拉電阻的取值需要綜合考慮電路驅動能力、功耗、信號完整性、噪聲容限等多方面因素。以下是詳細的取值分析及方法&#xff1a; 一、下拉電阻的核心影響因素 1. 驅動能力與電流限制 單片機 IO 口驅動能力&#xff1a;如 STM32 的 IO 口在輸入模式下的漏電流通常很小&#xf…

NY271NY274美光科技固態NY278NY284

美光科技NY系列固態硬盤深度剖析&#xff1a;技術、市場與未來 技術前沿&#xff1a;232層NAND架構與性能突破 在存儲技術的賽道上&#xff0c;美光科技&#xff08;Micron&#xff09;始終是行業領跑者。其NY系列固態硬盤&#xff08;SSD&#xff09;憑借232層NAND閃存架構的…

微信開發者工具 插件未授權使用,user uni can not visit app

參考&#xff1a;https://www.jingpinma.cn/archives/159.html 問題描述 我下載了一個別人的小程序&#xff0c;想運行看看效果&#xff0c;結果報錯信息如下 原因 其實就是插件沒有安裝&#xff0c;需要到小程序平臺安裝插件。處理辦法如下 在 app.json 里&#xff0c;聲…

UE5 讀取配置文件

使用免費的Varest插件&#xff0c;可以讀取本地的json數據 獲取配置文件路徑&#xff1a;當前配置文件在工程根目錄&#xff0c;打包后在 Windows/項目名稱 下 讀取json 打包后需要手動復制配置文件到Windows/項目名稱 下

【kdump專欄】KEXEC機制中SME(安全內存加密)

【kdump專欄】KEXEC機制中SME&#xff08;安全內存加密&#xff09; 原始代碼&#xff1a; /* Ensure that these pages are decrypted if SME is enabled. */ 533 if (pages) 534 arch_kexec_post_alloc_pages(page_address(pages), 1 << order, 0);&#x1f4cc…