一文夯實併發編程的理論基礎

来源:https://www.cnblogs.com/seven97-top/p/18428034
-Advertisement-
Play Games

JMM記憶體模型 定義 java記憶體模型(即 java Memory Model,簡稱JMM),不存在的東西,是一個概念,約定 主要分成兩部分來看,一部分叫做主記憶體,另一部分叫做工作記憶體。 java當中的共用變數;都放在主記憶體當中,如類的成員變數(實例變數),還有靜態的成員變數(類變數),都是存儲在主 ...


JMM記憶體模型

定義

java記憶體模型(即 java Memory Model,簡稱JMM),不存在的東西,是一個概念,約定

主要分成兩部分來看,一部分叫做主記憶體,另一部分叫做工作記憶體。

  • java當中的共用變數;都放在主記憶體當中,如類的成員變數(實例變數),還有靜態的成員變數(類變數),都是存儲在主記憶體中的。每一個線程都可以訪問主記憶體;

  • 每一個線程都有其自己的工作記憶體,當線程要執行代碼的時候,就必須在工作記憶體中完成。比如線程操作共用變數,它是不能直接在主記憶體中操作共用變數的,只能夠將共用變數先複製一份,放到線程自己的工作記憶體當中,線程在其工作記憶體對該複製過來的共用變數處理完後,再將結果同步回主記憶體中去。

主記憶體是 所有線程都共用的,都能訪問的。所有的共用變數都存儲於主記憶體;共用變數主要包括類當中的成員變數,以及一些靜態變數等。局部變數是不會出現在主記憶體當中的,因為局部變數只能線程自己使用;工作記憶體每一個線程都有自己的工作記憶體,工作記憶體只存儲 該線程對共用變數的副本。線程對變數的所有讀寫操作都必須在工作記憶體中完成,而不能直接讀寫主記憶體中的變數,不同線程之間也不能直接訪問 對方工作記憶體中的 變數;線程對共用變數的操作都是對其副本進行操作,操作完成之後再同步回主記憶體當中去;

JMM的同步約定:

  • 線程解鎖前,必須把共用變數立刻刷回主存

  • 線程加鎖前,必須讀取主存中的最新值到工作記憶體中

  • 加鎖和解鎖是同一把鎖

也就是說,JMM是一種抽象的結構,它提供了合理的禁用緩存和禁止重排序的方案來解決可見性、有序性的問題

作用:主要目的就是在多線程對共用變數進行讀寫時,來保證共用變數的可見性、有序性、原子性;在編程當中是通過兩個關鍵字 synchronized 和 volatile 來保證共用變數的三個特性的。

主記憶體與工作記憶體交互

一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體的呢?

Java記憶體模型中定義了上圖中的 8 種操作(橙色箭頭)來完成,虛擬機實現時必須保證每一種操作都是原子的、不可再分的。

舉個例子:假設現線上程1想要來訪問主記憶體當中的共用變數 x ,即當前主記憶體中的共用變數x的取值為 boolean x = true;

  1. 線程1首先會做一個原子操作叫做Read,讀取主記憶體當中的共用變數x的取值,即 boolean x = true;
  2. 接下來就是 Load 操作,把在主記憶體中讀取到的共用變數載入到了工作記憶體當中(副本);
  3. 接著執行 Use 操作,如果線程1需要對共用變數x進行操作,即會取到從主記憶體中載入過來的共用變數x的取值去進行一些操作;
  4. 操作之後會有一個新的結果返回,假設令這個共用變數的取值變為false,完成 Assign 操作,即給共用變數x賦新值;
  5. 操作完成之後;就需要同步回主記憶體,首先會完成一個 Store 的原子操作,來保存這個處理結果;
  6. 接著執行Write操作,即在工作記憶體中,Assign 賦值給共用變數的值同步到主記憶體當中,主記憶體中共用變數取值x由true更改為false。
  7. 另外還有兩個與鎖相關的操作,Lock與unlock,比如說加了synchronized,才會產生有lock與unlock操作;如果對共用變數的操作沒有加鎖,那麼也就不會有lock與unlock操作。

註意:如果對共用變數執行 lock 操作,該線程就會去主記憶體中獲取到共用變數的最新值,刷新工作記憶體中的舊值,保證可見性;(加鎖說明要對這個共用變數進行寫操作了,先刷新舊值,再操作新值)對共用變數執行 unlock 操作,必須先把此變數同步回主記憶體中,再執行 unlock;(因為對共用變數釋放鎖,接下來其他線程就能訪問到這個共用變數,就必須使這個共用變數呈現的是最新值)這兩點就是 synchronized為什麼能保證“可見性”的原因。

規則:

  1. 不允許read、load、store、write操作之一單獨出現,也就是read操作後必須load,store操作後必須write。
  2. 不允許線程丟棄他最近的assign操作,即工作記憶體中的變數數據改變了之後,必須告知主存。
  3. 不允許線程將沒有assign的數據從工作記憶體同步到主記憶體。
  4. 一個新的變數必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變數。就是對變數實施use、store操作之前,必須經過load和assign操作。
  5. 一個變數同一時間只能有一個線程對其進行lock操作。多次lock之後,必須執行相同次數unlock才可以解鎖。
  6. 如果對一個變數進行lock操作,會清空所有工作記憶體中此變數的值。在執行引擎使用這個變數前,必須重新load或assign操作初始化變數的值。
  7. 如果一個變數沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變數。
  8. 一個線程對一個變數進行unlock操作之前,必須先把此變數同步回主記憶體。

總結

主記憶體 與 工作記憶體 之間的 數據交互過程(即主記憶體與工作記憶體的交互是通過這8個原子操作來保證數據的正確性的):lock → read → load → use → assign → store → write → unlock

併發編程中的三個問題

線程不安全示例

// 案例演示:5個線程各執行1000次i++操作:
public class Test01Atomicity {
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        // 5個線程都執行1000次 i++        
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };        // 5個線程
        ArrayList<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            ts.add(t);
        }
        for (Thread t : ts) {
            t.join();
        }        
        /* 最終的效果即,加出來的效果不是5000,可能會少於5000        
        那麼原因就在於 i++ 並不是一個原子操作        
        下麵會通過java反彙編的方式來進行演示和分析,這個 i++ 其實有4條指令    */
        System.out.println("number = " + number);
    }
}

可見性:CPU緩存引起

可見性:是指當一個線程對共用變數進行了修改,那麼另外的線程可以立即看到修改後的最新值。

//線程1執行的代碼
int i = 0;
i = 10;
 
//線程2執行的代碼
j = i;

假設執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值載入到CPU1的高速緩存中,然後賦值為10,那麼在CPU1的高速緩存當中i的值變為10了,卻沒有立即寫入到主存當中。

此時線程2執行 j = i,它會先去主存讀取i的值並載入到CPU2的緩存當中,註意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10.

解決可見性:

  1. 在共用變數前面加上volatile關鍵字修飾;volatile 的底層實現原理是記憶體屏障(Memory Barrier),保證了對 volatile 變數的寫指令後會加入寫屏障,對 volatile 變數的讀指令前會加入讀屏障。

    • 寫屏障(sfence)保證在寫屏障之前的,對共用變數的改動,都同步到主存當中;

    • 讀屏障(lfence)保證在讀屏障之後,對共用變數的讀取,載入的是主存中最新數據;

  2. ,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變數的修改刷新到主存當中。這是因為synchronized 同步時會對應 JMM 中的 lock 原子操作,lock 操作會刷新工作記憶體中的變數的值,得到共用記憶體(主記憶體)中最新的值,從而保證可見性。

    • synchronized 同步的時候會對應8個原子操作當中的 lock 與 unlock 這兩個原子操作,lock操作執行時該線程就會去主記憶體中獲取到共用變數最新值,刷新工作記憶體中的舊值,從而保證可見性。

原子性: 分時復用引起

原子性(Atomicity): 在一次或多次操作中,要麼所有的操作都執行,並且不會受其他因素干擾而中斷,要麼所有的操作都不執行;

int i = 1;

// 線程1執行
i += 1;

// 線程2執行
i += 1;

這裡需要註意的是:i += 1需要三條 CPU 指令

  1. 將變數 i 從記憶體讀取到 CPU寄存器;
  2. 在CPU寄存器中執行 i + 1 操作;
  3. 將最後的結果i寫入記憶體(緩存機制導致可能寫入的是 CPU 緩存而不是記憶體)。

由於CPU分時復用(線程切換)的存在,線程1執行了第一條指令後,就切換到線程2執行,假如線程2執行了這三條指令後,再切換會線程1執行後續兩條指令,將造成最後寫到記憶體中的i值是2而不是3。

x = 10;        //語句1: 直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工作記憶體中
y = x;         //語句2: 包含2個操作,它先要去讀取x的值,再將x的值寫入工作記憶體,雖然讀取x的值以及 將x的值寫入工作記憶體 這2個操作都是原子性操作,但是合起來就不是原子性操作了。
x++;           //語句3: x++包括3個操作:讀取x的值,進行加1操作,寫入新的值。
x = x + 1;     //語句4: 同語句3

上面4個語句只有語句1的操作具備原子性。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變數,變數之間的相互賦值不是原子操作)才是原子操作。

解決原子性:

Java記憶體模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

有序性: 重排序引起

有序性(Ordering):是指程式代碼在執行過程中的先後順序,由於java在編譯器以及運行期的優化,導致了代碼的執行順序未必就是開發者編寫代碼的順序。

int i = 0;              
boolean flag = false;
i = 1;                //語句1  
flag = true;          //語句2

為什麼要重排序?一般會認為編寫代碼的順序就是代碼最終的執行順序,那麼實際上並不一定是這樣的,為了提高程式的執行效率,java在編譯時和運行時會對代碼進行優化(JIT即時編譯器),會導致程式最終的執行順序不一定就是編寫代碼時的順序。重排序 是指 編譯器 和 處理器 為了優化程式性能 而對 指令序列 進行 重新排序 的一種手段;

從 java 源代碼到最終實際執行的指令序列,會分別經歷下麵三種重排序:

  1. 編譯器優化的重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由於處理器使用緩存和讀 / 寫緩衝區,這使得載入和存儲操作看上去可能是在亂序執行。

上述的 1 屬於編譯器重排序,2 和 3 屬於處理器重排序。這些重排序都可能會導致多線程程式出現記憶體可見性問題。對於編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM 的處理器重排序規則會要求 java 編譯器在生成指令序列時,插入特定類型的記憶體屏障(memory barriers,intel 稱之為 memory fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

解決有序性:

  1. 可以使用 synchronized 同步代碼塊來保證有序性;加了synchronized,依然會發生指令重排序(可以看看DCL單例模式),只不過,由於存在同步代碼塊,可以保證只有一個線程執行同步代碼塊當中的代碼,也就能保證有序性。

  2. 給共用變數加volatile關鍵字來解決有序性問題。

    • 寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之後;

    • 讀屏障會確保指令重排序時,不會將讀屏障之後的代碼排在讀屏障之前;

Happens-Before 規則

Happens-Before是一種可見性規則,它表達的含義是前面一個操作的結果對後續操作是可見的。解釋為 “先行發生於...”

A happens-before B,也就意味著A的執行結果對B是可見的

單一線程(程式順序)原則

Single Thread rule:在一個線程內,在程式前面的操作先行發生於後面的操作。

as-id-serio 語義

管程鎖定(監視器鎖)規則

Monitor Lock Rule :對一個鎖的解鎖 Happens-Before 於後續對這個鎖 的加鎖

volatile 變數規則

Volatile Variable Rule:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀

線程啟動start規則

Thread Start Rule:Thread 對象的 start() 方法調用先行發生於此線程的每一個動作。

如果線程A執行操作ThreadB.start()(啟動線程B),那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作

線程加入join規則

Thread Join Rule:Thread 對象的結束先行發生於 join() 方法返回。

如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回

線程中斷規則

Thread Interruption Rule:對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷發生。

對象終結規則

Finalizer Rule:一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。

傳遞性

Transitivity:如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那麼操作 A 先行發生於操作 C。

安全發佈對象

發佈與逃逸

發佈的意思是使一個對象能夠被當前範圍之外的代碼所使用

public static Hashset<Person> persons;
public void init(){
    persons = new HashSet<Person>;
}

不安全發佈:私有數組,但外部範圍也能使用,導致不安全發佈

private string[] states = {"a","b","c","d"};
//發佈出去一個
public string[] getstates(){
    return states;
}

public static void main(string[] args){
    App unSafePub = new App();
    System.out.printIn("Init array is:" + Arrays.tostring(unsafePub.getstates()));
    unsafePub.getstates()[0] = "Seven!";
    System.out.printin("After modify.the array is: " + Arrays.tostring(unsafePub.getstates()));
}

對象溢出:

一種錯誤的發佈,當一個對象還沒有構造完成時,就使它被其他線程所見

public cass FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample() {
        i = 1; //1.寫fina]域
        obj = this; //2.this 引用"逃逸"
    }
    
    public static void writer() {
        new FinalReferenceEscapeExample();
    }
    public static void reader() {
        if(obj != null){    //3.
            int temp = obj.i;   //4.
        }
    }
}

逃逸帶來的問題

安全發佈對象的四種方法

  • 在靜態初始化函數中初始化一個對象引用

  • 將對象的引用保存到volatile類型的域或者AtomicReference對象中(利用volatile happen-before規則)

  • 將對象的引用保存到某個正確構造對象的final類型域中(初始化安全性)

  • 將對象的引用保存到一個由鎖保護的域中(讀寫都上鎖)

線程安全的實現方法

互斥同步

synchronized 和 ReentrantLock。

非阻塞同步

互斥同步最主要的問題就是線程阻塞和喚醒所帶來的性能問題,因此這種同步也稱為阻塞同步。

互斥同步屬於一種悲觀的併發策略,總是認為只要不去做正確的同步措施,那就肯定會出現問題。無論共用數據是否真的會出現競爭,它都要進行加鎖(這裡討論的是概念模型,實際上虛擬機會優化掉很大一部分不必要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程需要喚醒等操作。

CAS

隨著硬體指令集的發展,我們可以使用基於衝突檢測的樂觀併發策略: 先進行操作,如果沒有其它線程爭用共用數據,那操作就成功了,否則採取補償措施(不斷地重試,直到成功為止)。這種樂觀的併發策略的許多實現都不需要將線程阻塞,因此這種同步操作稱為非阻塞同步。

樂觀鎖需要操作和衝突檢測這兩個步驟具備原子性,這裡就不能再使用互斥同步來保證了,只能靠硬體來完成。硬體支持的原子性操作最典型的是: 比較並交換(Compare-and-Swap,CAS)。CAS 指令需要有 3 個操作數,分別是記憶體地址 V、舊的預期值 A 和新值 B。當執行操作時,只有當 V 的值等於 A,才將 V 的值更新為 B。

AtomicInteger

J.U.C 包裡面的整數原子類 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 類的 CAS 操作。

以下代碼使用了 AtomicInteger 執行了自增的操作。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}

以下代碼是 incrementAndGet() 的源碼,它調用了 unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代碼是 getAndAddInt() 源碼,var1 指示對象記憶體地址,var2 指示該欄位相對對象記憶體地址的偏移,var4 指示操作需要加的數值,這裡為 1。通過 getIntVolatile(var1, var2) 得到舊的預期值,通過調用 compareAndSwapInt() 來進行 CAS 比較,如果該欄位記憶體地址中的值等於 var5,那麼就更新記憶體地址為 var1+var2 的變數為 var5+var4。

可以看到 getAndAddInt() 在一個迴圈中進行,發生衝突的做法是不斷的進行重試。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

ABA

如果一個變數初次讀取的時候是 A 值,它的值被改成了 B,後來又被改回為 A,那 CAS 操作就會誤認為它從來沒有被改變過。

J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題,它可以通過控制變數值的版本來保證 CAS 的正確性。大部分情況下 ABA 問題不會影響程式併發的正確性,如果需要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。

無同步方案

要保證線程安全,並不是一定就要進行同步。如果一個方法本來就不涉及共用數據,那它自然就無須任何同步措施去保證正確性。

棧封閉

多個線程訪問同一個方法的局部變數時,不會出現線程安全問題,因為局部變數存儲在虛擬機棧中,屬於線程私有的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}

線程本地存儲(Thread Local Storage)

如果一段代碼中所需要的數據必須與其他代碼共用,那就看看這些共用數據的代碼是否能保證在同一個線程中執行。如果能保證,就可以把共用數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

符合這種特點的應用並不少見,大部分使用消費隊列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程儘量在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的“一個請求對應一個伺服器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多 Web 服務端應用都可以使用線程本地存儲來解決線程安全問題。

可以使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。

對於以下代碼,thread1 中設置 threadLocal 為 1,而 thread2 設置 threadLocal 為 2。過了一段時間之後,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            threadLocal.remove();
        });
        thread1.start();
        thread2.start();
    }
}

ThreadLocal 從理論上講並不是用來解決多線程併發問題的,因為根本不存在多線程競爭。

在一些場景 (尤其是使用線程池) 下,由於 ThreadLocal.ThreadLocalMap 的底層數據結構導致 ThreadLocal 有記憶體泄漏的情況,應該儘可能在每次使用 ThreadLocal 後手動調用 remove(),以避免出現 ThreadLocal 經典的記憶體泄漏甚至是造成自身業務混亂的風險。

可重入代碼(Reentrant Code)

這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程式不會出現任何錯誤。

可重入代碼有一些共同的特征,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。

關於作者

來自一線程式員Seven的探索與實踐,持續學習迭代中~

本文已收錄於我的個人博客:https://www.seven97.top

公眾號:seven97,歡迎關註~

本文來自線上網站:seven的菜鳥成長之路,作者:seven,轉載請註明原文鏈接:www.seven97.top


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 強一致性 1.1. 最終一致資料庫通過跨多台機器分區和複製數據集來獲得可擴展性,其代價是要跨副本維持強數據一致性以及允許衝突寫入 1.1.1. 在更新數據對象後,不同的客戶端可能會看到該對象的舊值或新值,直到所有副本都收斂到最新值 1.2. 另一類分散式資料庫提供一種可替代的模型,即強一致性數 ...
  • 1. 最終一致性 1.1. 在一些應用領域,通常談論的是銀行和金融行業,最終一致性根本不合適 1.2. 事實上,最終一致性在銀行業已經使用了很多年 1.2.1. 支票需要幾天時間才能在你的賬戶上進行核對,而且你可以輕鬆地開出比賬戶餘額多的支票 1.2.2. 當處理檢查並建立一致性後,你才能看到一些後 ...
  • 大家好,我是湯師爺~ 今天聊聊SaaS架構中的流程架構分析。 業務流程的概念 業務流程是企業為實現目標而制定的一套系統化的工作方法。它由一系列有序的業務活動組成,按照既定規則將資源(輸入)轉化為有價值的結果(輸出)。這一過程需結合企業的具體情況和可用資源,旨在為客戶創造價值,同時達成企業目標。 通過 ...
  • 1. 可擴展資料庫基礎 1.1. 絕大多數應用程式都是基於關係資料庫技術構建的 1.2. 資料庫必須存儲大量數據,為分佈在全球的客戶端提供快速的查詢響應,並且全天候可用 1.3. NoSQL資料庫採用簡單的數據模型,可以複製和分區以支持海量數據集和請求量 1.4. Facebook以使用MySQL管 ...
  • 1. 微服務 1.1. 微服務的起源可以追溯到2008年左右 1.1.1. 在Amazon,​“兩個比薩原則”成為一個單系統組件團隊規模的管理原則,後來被稱為微服務 1.1.1.1. 每個內部團隊都應該小到可以用兩個比薩餅喂飽 1.1.2. Amazon和Netflix是微服務架構的先驅,他們在20 ...
  • 1. 無伺服器的魅力 1.1. 對於某些應用程式,負載在工作時間可能很高,而在非工作時間可能很低或者不存在 1.2. 其他應用程式後臺流量可能在99%的時間里都很低 1.2.1. 一旦到了一些大型節目的門票發佈時間,負載需求可能會在數小時內飆升至平均水平的10000倍,然後回落至正常水平 1.3.  ...
  • 1. 非同步消息傳遞 1.1. 通信是分散式系統的基礎,也是架構師需要納入其系統設計的主要問題 1.2. 客戶端發送請求並等待伺服器響應 1.2.1. 這就是大多數分散式通信的設計方式,因為客戶端需要得到即時響應後才能繼續 1.2.2. 並非所有系統都有這個要求 1.3. 使用非同步通信的方式,客戶端( ...
  • 在Python中,協議(Protocol)和介面(Interface)是用於定義類和對象之間交互的一種方式,特別是在實現多態性和代碼可重用性時,協議是一種抽象概念,描述了對象所需實現的方法和屬性,而不關心具體的類或實現。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...