萬字詳解 Java 線程安全,面試必備!

来源:https://www.cnblogs.com/javastack/archive/2022/11/21/16911944.html
-Advertisement-
Play Games

來源:blog.csdn.net/u014454538/article/details/98515807 1. Java中的線程安全 Java線程安全:狹義地認為是多線程之間共用數據的訪問。 Java語言中各種操作共用的數據有5種類型:不可變、絕對線程安全、相對線程安全、線程相容、線程獨立 ① 不可 ...


來源:blog.csdn.net/u014454538/article/details/98515807

1. Java中的線程安全

  • Java線程安全:狹義地認為是多線程之間共用數據的訪問。
  • Java語言中各種操作共用的數據有5種類型:不可變、絕對線程安全、相對線程安全、線程相容、線程獨立

① 不可變

  • 不可變(Immutable) 的對象一定是線程安全的,不需要再採取任何的線程安全保障措施。
  • 只要能正確構建一個不可變對象,該對象永遠不會在多個線程之間出現不一致的狀態。
  • 多線程環境下,應當儘量使對象成為不可變,來滿足線程安全。

如何實現不可變?

  • 如果共用數據是基本數據類型,使用final關鍵字對其進行修飾,就可以保證它是不可變的。
  • 如果共用數據是一個對象,要保證對象的行為不會對其狀態產生任何影響。
  • String是不可變的,對其進行substring()、replace()、concat()等操作,返回的是新的String對象,原始的String對象的值不受影響。而如果對StringBuffer或者StringBuilder對象進行substring()、replace()、append()等操作,直接對原對象的值進行改變。
  • 要構建不可變對象,需要將內部狀態變數定義為final類型。如java.lang.Integer類中將value定義為final類型。

Java 面試題最全整理:https://www.javastack.cn/mst/

private final int value;

常見的不可變的類型:

  • final關鍵字修飾的基本數據類型
  • 枚舉類型、String類型
  • 常見的包裝類型:Short、Integer、Long、Float、Double、Byte、Character等
  • 大數據類型:BigInteger、BigDecimal

註意:原子類 AtomicInteger 和 AtomicLong 則是可變的。

對於集合類型,可以使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。

  • 通過Collections.unmodifiableMap(map)獲的一個不可變的Map類型。
  • Collections.unmodifiableXXX() 先對原始的集合進行拷貝,需要對集合進行修改的方法都直接拋出異常。

例如,如果獲得的不可變map對象進行put()、remove()、clear()操作,則會拋出UnsupportedOperationException異常。

② 絕對線程安全

絕對線程安全的實現,通常需要付出很大的、甚至不切實際的代價。

Java API中提供的線程安全,大多數都不是絕對線程安全。

例如,對於數組集合Vector的操作,如get()、add()、remove()都是有synchronized關鍵字修飾。有時調用時也需要手動添加同步手段,保證多線程的安全。

下麵的代碼看似不需要同步,實際運行過程中會報錯。

import java.util.Vector;

/**
 * @Author: lucy
 * @Version 1.0
 */
public class VectorTest {
    public static void main(String[] args) {
        Vector<Integer> vector = new Vector<>();
        while(true){
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=0;i<vector.size();i++){
                        System.out.println("刪除vector中的第" + i+"個元素");
                        vector.remove(i);
                    }
                }
            }).start();
            while (Thread.activeCount()>20)
                return;
        }
    }
}

出現ArrayIndexOutOfBoundsException異常,原因:某個線程恰好刪除了元素i,使得當前線程無法訪問元素i。

Exception in thread "Thread-1109" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1
 at java.util.Vector.remove(Vector.java:831)
 at VectorTest$2.run(VectorTest.java:28)
 at java.lang.Thread.run(Thread.java:745)

需要將對元素的get和remove構造成同步代碼塊:

synchronized (vector){
    for (int i = 0; i < vector.size(); i++) {
        System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
    }
}
synchronized (vector){
    for (int i=0;i<vector.size();i++){
        System.out.println("刪除vector中的第" + i+"個元素");
        vector.remove(i);
    }
}

③ 相對線程安全

  • 相對線程安全需要保證對該對象的單個操作是線程安全的,在必要的時候可以使用同步措施實現線程安全。
  • 大部分的線程安全類都屬於相對線程安全,如Java容器中的Vector、HashTable、通過Collections.synchronizedXXX()方法包裝的集合。

④ 線程相容

  • Java中大部分的類都是線程相容的,通過添加同步措施,可以保證在多線程環境中安全使用這些類的對象。
  • 如常見的ArrayList、HashTableMap都是線程相容的。

⑤ 線程對立

  • 線程對立是指:無法通過添加同步措施,實現多線程中的安全使用。
  • 線程對立的常見操作有:Thread類的suspend()和resume()(已經被JDK聲明廢除),System.setIn()System.setOut()等。

2. Java的枚舉類型

通過enum關鍵字修飾的數據類型,叫枚舉類型。

  • 枚舉類型的每個元素都有自己的序號,通常從0開始編號。
  • 可以通過values()方法遍歷枚舉類型,通過name()或者toString()獲取枚舉類型的名稱
  • 通過ordinal()方法獲取枚舉類型中元素的序號
public class EnumData {
    public static void main(String[] args) {
        for (Family family : Family.values()) {
            System.out.println(family.name() + ":" + family.ordinal());
        }
    }
}

enum Family {
    GRADMOTHER, GRANDFATHER, MOTHER, FATHER, DAUGHTER, SON;
}

可以將枚舉類型看做普通的class,在裡面定義final類型的成員變數,便可以為枚舉類型中的元素賦初值。

要想獲取枚舉類型中元素實際值,需要為成員變數添加getter方法。

雖然枚舉類型的元素有了自己的實際值,但是通過ordinal()方法獲取的元素序號不會發生改變。

public class EnumData {
    public static void main(String[] args) {
        for (Family family : Family.values()) {
            System.out.println(family.name() + ":實際值" + family.getValue() +
                    ", 實際序號" + family.ordinal());
        }
    }
}
enum Family {
    GRADMOTHER(3), GRANDFATHER(4), MOTHER(1), FATHER(2), DAUGHTER(5), SON(6);
    private final int value;
    Family(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}

3. Java線程安全的實現

① 互斥同步

互斥同步(Mutex Exclusion & Synchronization)是一種常見的併發正確性保障手段。

  • 同步:多個線程併發訪問共用數據,保證共用數據同一時刻只被一個(或者一些,使用信號量)線程使用。
  • 互斥:互斥是實現同步的一種手段,主要的互斥實現方式:臨界區(Critical Section)、互斥量(Mutex)、信號量(Semaphore)。

同步與互斥的關係:

  • 互斥是原因,同步是結果。
  • 同步是目的,互斥是方法。

Java中,最基本的實現互斥同步的手段是synchronized關鍵字,其次是JUC包中的ReentrantLock。

關於synchronized關鍵字:

  • 編譯後的同步塊,開始處會添加monitorenter指令,結束處或異常處會添加monitorexit指令。
  • monitorenter和monitorexit指令中都包含一個引用類型的參數,分別指向加鎖或解鎖的對象。如果是同步代碼塊,則為synchronized括弧中明確指定的對象;如果為普通方法,則為當前實例對象;如果為靜態方法,則為類對應的class對象。
  • JVM執行monitorenter指令時,要先嘗試獲取鎖:如果對象沒被鎖定或者當前線程已經擁有該對象的鎖,則鎖計數器加1;否則獲取鎖失敗,進入阻塞狀態,等待持有鎖的線程釋放鎖。
  • JVM執行monitorexit指令時,鎖計數器減1,直到計數器的值為0,鎖被釋放。(synchronized是支持重進入的)
  • 由於阻塞或者喚醒線程都需要從用戶態(User Mode)切換到核心態(Kernel Mode),有時鎖只會被持有很短的時間,沒有必要進行狀態轉換。可以讓線程在阻塞之前先自旋等待一段時間,超時未獲取到鎖才進入阻塞狀態,這樣可以避免頻繁的切入到核心態。其實,就是後面自旋鎖的思想。

關於ReentrantLock:

  • 與synchronized關鍵字相比,它是API層面的互斥鎖(lock()、unlock()、try...finally)。
  • 與synchronized關鍵字相比,具有可中斷、支持公平與非公平性、可綁定多個Condition對象的高級功能。
  • 由於synchronized關鍵字被優化,二者的性能差異並不是很大,如果不是想使用ReentrantLock的高級功能,優先考慮使用synchronized關鍵字。

② 非阻塞同步

(1)CAS概述

互斥同步最大的性能問題是線程的阻塞和喚醒,因此又叫阻塞同步。

互斥同步採用悲觀併發策略:

  • 多線程併發訪問共用數據時,總是認為只要不加正確的同步措施,肯定會出現問題。
  • 無論共用數據是否存在競爭,都會執行加鎖、用戶態和心態的切換、維護鎖計數器、檢查是否有被阻塞的線程需要喚醒等操作。

隨著硬體指令集的發展,我們可以採用基於衝突檢測的樂觀併發策略:

  • 先進行操作,如果不存在衝突(即沒有其他線程爭用共用數據),則操作成功。
  • 如果有其他線程爭用共用數據,產生了衝突,使用其他的補償措施。
  • 常見的補償措施:不斷嘗試,直到成功為止,比如迴圈的CAS操作。

樂觀併發策略的許多實現都不需要將線程阻塞,這種同步操作叫做非阻塞同步。

非阻塞同步依靠的硬體指令集:前三條是比較久遠的指令,後兩條是現代處理器新增的。

  • 測試和設置(Test and Set)
  • 獲取並增加(Fetch and Increment)
  • 交換(Swap)
  • 比較並交換(Compare and Swap,即CAS)
  • 載入鏈接/條件存儲(Load Linked/ Store Conditional,即LL/SC)

什麼是CAS?

  • CAS,即Compare and Swap,需要藉助處理器的cmpxchg指令完成。
  • CAS指令需要三個操作數:記憶體位置V(Java中可以簡單的理解為變數的記憶體地址)、舊的期待值A、新值B。
  • CAS指令執行時,當且僅當V符合舊的預期值A,處理器才用新值B更新V的值;否則,不執行更新。
  • 不管是否更新V的值,都返回V的舊值,整個處理過程是一個原子操作。

原子操作:所謂的原子操作是指一個或一系列不可被中斷的操作。

Java中的CAS操作:

  • Java中的CAS操作由sun.misc.Unsafe中的compareAndSwapInt()、compareAndSwapLong()等幾個方法包裝提供。實際無法調用這些方法,需要採用反射機制才能使用。
  • 在實際的開發過程中,一般通過其他的Java API調用它們,如JUC包原子類中的compareAndSet(expect, update) 、getAndIncrement()等方法。這些方法內部都使用了Unsafe類的CAS操作。
  • Unsafe類的CAS操作,通過JVM的即時編譯器編譯後,是一條與平臺相關的CAS指令。

除了偏向鎖,Java中其他鎖的實現方式都是用了迴圈的CAS操作。

(2)通過迴圈的CAS實現原子操作

通過++i或者i++可以實現計數器的自增,在多線程環境下,這樣使用是非線程安全的。

public class UnsafeCount {
    private int i = 0;
    private static final int THREADS_COUNT = 200;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        UnsafeCount counter = new UnsafeCount();
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        counter.count();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println("多線程調用計數器i,運行後的值為: " + counter.i);
    }

    public void count() {
        i++;
    }
}

運行以上的代碼發現:當線程數量增加,每個線程調用計數器的次數變大時,每次運行的結果是錯誤且不固定的。

為了實現實在一個多線程環境下、線程安全的計數器,需要使用AtomicInteger的原子自增運算。

import java.util.concurrent.atomic.AtomicInteger;
public class SafeCount {
    private AtomicInteger atomic = new AtomicInteger(0);
    private static final int THREAD_COUNT = 200;
    public static void main(String[] args) {
        SafeCount counter = new SafeCount();
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0;j<10000;j++){
                        counter.count();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println("多線程調用線程安全的計數器atomic:"+counter.atomic);
    }
    public void count() {
        // 調用compareAnSet方法,使用迴圈的CAS操作實現計數器的原子自增
        for (; ; ) {
            int expect = atomic.get();
            int curVal = expect + 1;
            if (atomic.compareAndSet(expect, curVal)) {
                break;
            }
        }
    }
}

與非線程安全的計數器相比,線程安全的計數器有以下特點:

  • 將int類型的計數器變數i,更換成具有CAS操作的AtomicInteger類型的計數器變數atomic。
  • 進行自增運算時,通過迴圈的CAS操作實現atomic的原子自增。
  • 先通過atomic.get()獲取expect的值,將expect加一得到新值,然後通過atomic.compareAndSet(expect, curVal)這一方法實現CAS操作。
  • 其中compareAndSet()返回的true或者false,表示此次CAS操作是否成功。如果返回false,則不停地重覆執行CAS操作,直到操作成功。

上面的count方法實現的AtomicInteger原子自增,可以只需要調用incrementAndGet()一個方法就能實現。

public void count() {
    // 調用incrementAndGet方法,實現AtomicInteger的原子自增
    atomic.incrementAndGet();
}

因為incrementAndGet()方法,封裝了通過迴圈的CAS操作實現AtomicInteger原子自增的代碼。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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;
}
(3)CAS操作存在的問題

1. ABA問題

  • 在執行CAS操作更新共用變數的值時,如果一個值原來是A,被其他線程改成了B,然後又改回成了A。對於該CAS操作來說,它完全感受不到共用變數值的變化。這種操作漏洞稱為CAS操作的ABA問題。
  • 解決該問題的思路是,為變數添加版本號,每次更新時版本號遞增。這種場景下就成了1A --> 2B --> 3A。CAS操作就能檢測到共用變數的ABA問題了。
  • JUC包中,也提供了相應的帶標記的原子引用類AtomicStampedReference來解決ABA問題。
  • AtomicStampedReference的compareAndSet()方法會首先比較期待的引用是否等於當前引用,然後檢查期待的標記是否等於當前標記。如果全部相等,則以原子操作的方式將新的引用和新的標記更新到當前值中。
  • 但是AtomicStampedReference目前比較雞肋,如果想解決AB問題,可以使用鎖。

2. 迴圈時間過長,開銷大

迴圈的CAS操作如果長時間不成功,會給CPU帶來非常大的執行開銷。

3. 只能保證一個共用變數的原子操作

  • 只對一個共用變數執行操作時,可以通過迴圈的CAS操作實現。如果是多個共用變數,迴圈的CAS操作無法保證操作的原子性。
  • 取巧的操作:將多個共用變數合為一個變數進行CAS操作。JDK1.5開始,提供了AtomicReference類保證引用對象之間的原子性,可以將多個變數放在一個對象中進行CAS操作。

③ 無同步方案

同步只是保證共用數據爭用時正確性的一種手段,如果不存在共用數據,自然無須任何同步措施。

(1)棧封閉

多個線程訪問同一個方法的局部變數時,不會出現線程安全問題。

因為方法中的局部變數不會逃出該方法而被其他線程訪問,因此可以看做JVM棧中數據,屬於線程私有。

(2)可重入代碼(Reentrant Code)

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

所有可重入的代碼都是線程安全,並非所有線程安全的代碼都是可重入的。

可重入代碼的共同特征:

  • 不依賴存儲在堆上的數據和公用的系統資源
  • 用到的狀態量都由參數中傳入
  • 不調用非可重用的方法

如何判斷代碼是否具備可重入性?如果一個方法,它的返回結果是可預測的。只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性,當然也就是線程安全的。

(3)線程本地存儲(TLS)

線程本地存儲(Thread Local Storage):

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

TLS的重要應用實例:經典的Web交互模型中,一個請求對應一個伺服器線程,使得Web伺服器應用可以使用。

Java中沒有關鍵字可以將一個變數定義為線程所獨享,但是Java中創建了java.lang.ThreadLocal類提供線程本地存儲功能。

  • 每一個線程內部都包含一個ThreadLocalMap對象,該對象將ThreadLocal對象的hashCode值作為key,即ThreadLocal.threadLocalHashCode,將本地線程變數作為value,構成鍵值對。
  • ThreadLocal對象是當前線程ThreadLocalMap對象的訪問入口,通過threadLocal.set()為本地線程添加獨享變數;通過threadLocal.get()獲取本地線程獨享變數的值。
  • ThreadLocal、ThreadLocalMap、Thread的關係:Thread對象中包含ThreadLocalMap對象,ThreadLocalMap對象中包含多個鍵值對,每個鍵值對的key是ThreadLocal對象的hashCode,value是本地線程變數。

ThreadLocal的編程實例:

  • 想為某個線程添加本地線程變數,必須通過ThreadLocal對象在該線程中進行添加,構造出的鍵值對自動存入該線程的map中;
  • 想要獲取某個線程的本地線程變數,必須在該線程中獲取,會自動查詢該線程的map,獲得ThreadLocal對象對應的value。
  • 通過ThreadLocal對象重覆為某個線程添加鍵值對,會覆蓋之前的value。
public class TLS {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 設置當前線程的本地線程變數
                threadLocal1.set("thread1");
                threadLocal2.set(1);
                System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                // 使用完畢後要刪除,避免記憶體泄露
                threadLocal1.remove();
                threadLocal2.remove();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal1.set("thread2");
                threadLocal2.set(2);
                System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                threadLocal1.remove();
                threadLocal2.remove();
            }
        });
        thread1.start();
        thread2.start();
        // 沒有通過ThreadLocal為主線程添加過本地線程變數,獲取到的內容都是null
        System.out.println(threadLocal1.get()+": "+threadLocal2.get());
    }
}

對ThreadLocal的正確理解:

  • ThreadLocal適用於線程需要有自己的實例變數,該實例變數可以在多個方法中被使用,但是不能被其他線程共用的場景。
  • 由於不存在數據共用,何談同步?因此ThreadLocal 從理論上講,不是用來解決多線程併發問題的。

ThreadLocal的實現:

最原始的想法:ThreadLocal維護線程與實例的映射。既然通過ThreadLocal對象為線程添加本地線程變數,那就將ThreadLocalMap放在ThreadLocal中。

原始想法存在的缺陷:多線程併發訪問ThreadLocal中的Map,需要添加鎖。這是, JDK 未採用該方案的一個原因。

優化後的方法:Thread維護ThreadLocal與實例的映射。Map是每個線程所私有,只能在當前線程通過ThreadLocal對象訪問自身的Map。不存在多線程併發訪問同一個Map的情況,也就不需要鎖。

優化後存在記憶體泄露的情況:JDK1.8中,ThreadLocalMap每個Entry對ThreadLocal對象是弱引用,對每個實例是強引用。當ThreadLocal對象被回收後,該Entry的鍵變成null,但Entry無法被移除。使得實例被Entry引用無法回收,造成記憶體泄露。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


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

-Advertisement-
Play Games
更多相關文章
  • 我們結合運算符重載知識實現string 類 在自己實現的String類中可以參考C++中string的方法 例如構造,加法,大小比較,長度,[] 等操作. 當前的MyString 類中,暫時不加入迭代器,我們將在下一節中加入迭代器的代碼. #include <iostream> using name ...
  • Java異常 1.概念理解 異常(Exepotion)指程式運行過程中不期而至的各種狀況,它阻止了程式按照程式員的預期正常執行,這就是異常(開發過程中的語法錯誤和1.邏輯錯誤不是異常)。如文件找不到,網路連接失敗,非法參數等。 異常發生在程式運行期間,影響程式的正常執行。 2.常見異常分類 **檢查 ...
  • WEB開發會話技術02 6.Cookie的生命周期 預設情況下,Cookie只在瀏覽器的記憶體中存活,也就是說,當你關閉瀏覽器後,Cookie就會消失。但是也可以通過方法設置cookie的生存時間。 cookie的生命周期指的是如何管理cookie,什麼時候cookie被銷毀。 setMaxAge(i ...
  • 全局有序 在RocketMQ中,如果使消息全局有序,可以為Topic設置一個消息隊列,使用一個生產者單線程發送數據,消費者端也使用單線程進行消費,從而保證消息的全局有序,但是這種方式效率低,一般不使用。 局部有序 假設一個Topic分配了兩個消息隊列,生產者在發送消息的時候,可以對消息設置一個路由I ...
  • 目錄 一.簡介 二.freeglut + glew 三.glfw + glad 四.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL E ...
  • 一.小結 1.標識符是程式中事務的名稱 2.標誌符是由字母 數字 下劃線 和美元符號$構成的字元序列 3.標識符必須以字母或下劃線開頭,不能以數字開頭 4.標識符不能是保留字 5.標識符可以是任意長度 6.選擇描述性的標識符可提高程式的可讀性 7.使用變數存儲在程式中使用的數據 8.聲明變數就是告訴 ...
  • 大家好,歡迎來到 Crossin的編程教室 ! 前幾天,後臺老有小伙伴留言“愛心代碼”。這不是Crossin很早之前發過的內容嘛,怎麼最近突然又被人翻出來了?後來才知道,原來是一部有關程式員的青春偶像劇《點燃我,溫暖你》在熱播,而劇中有一段關於期中考試要用程式畫一個愛心的橋段。 於是出於好奇,Cro ...
  • 列表和字典的區別是列表可以通過索引來訪問值,而字典可以通過名稱來訪問各個值。 字典這種數據結構稱為映射(mapping),字典是Python中唯一內置映射類型,值不按照順序排列,而是存儲再鍵下麵。 其中鍵可以是數字、字元串或元組等不可變數據類型。 字典的用途 字典的名稱指出了這種數據結構的用途。日常 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...