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

来源: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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...