Java併發(7)- 你真的瞭解ReentrantReadWriteLock嗎?

来源:https://www.cnblogs.com/konck/archive/2018/09/17/9473615.html
-Advertisement-
Play Games

引言 在前幾篇文章中瞭解了ReentrantLock、Semaphore與CountDownLatch後,J.U.C包中基於AQS實現的併發工具類還剩一個比較重要的:讀寫鎖ReentrantReadWriteLock。讀寫鎖在Java面試過程中是一個經常性考的題目,他涉及到的知識點比較多,導致很多人 ...


引言

在前幾篇文章中瞭解了ReentrantLock、Semaphore與CountDownLatch後,J.U.C包中基於AQS實現的併發工具類還剩一個比較重要的:讀寫鎖ReentrantReadWriteLock。讀寫鎖在Java面試過程中是一個經常性考的題目,他涉及到的知識點比較多,導致很多人不能透徹的理解他。舉幾個面試中常見的問題:

  • ReentrantReadWriteLock和ReentrantLock的區別?
  • 你能用ReentrantReadWriteLock實現一個簡單的緩存管理嗎?
  • 你能自己實現一個簡單的讀寫鎖嗎?
  • ReentrantReadWriteLock會發生寫饑餓的情況嗎?如果發生,有沒有比較好的解決辦法?

上面的問題大家都能回答出來嗎?如果都能很好的回答,那麼這篇文章也許對你沒有太大幫助。如果不能,彆著急,下麵就來一一分析解答上面的幾個問題。

1. ReentrantReadWriteLock和ReentrantLock的區別?

這個問題大家應該都會回答:ReentrantLock是獨占鎖,ReentrantReadWriteLock是讀寫鎖。那麼這就引申出下一個問題:ReentrantReadWriteLock中的讀鎖和寫鎖分別是獨占還是共用的?他們之間的關係又是什麼樣的?要瞭解透徹這兩個問題,分析源碼是再好不過的了。

1.1 ReentrantReadWriteLock中的讀鎖和寫鎖的實現方式

使用ReentrantReadWriteLock讀寫鎖的方式,會調用readLock()和writeLock()兩個方法,看下他們的源碼:

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

可以看到用到了WriteLock和ReadLock兩個靜態內部類,他們對鎖的實現如下:

public static class ReadLock implements Lock, java.io.Serializable {
    public void lock() {
        sync.acquireShared(1); //共用
    }

    public void unlock() {
        sync.releaseShared(1); //共用
    }
}

public static class WriteLock implements Lock, java.io.Serializable {
    public void lock() {
        sync.acquire(1); //獨占
    }

    public void unlock() {
        sync.release(1); //獨占
    }
}

abstract static class Sync extends AbstractQueuedSynchronizer {}

看到這裡發現了ReentrantReadWriteLock和ReentrantLock的一個相同點和不同點,相同的是使用了同一個關鍵實現AbstractQueuedSynchronizer,不同的是ReentrantReadWriteLock使用了兩個鎖分別實現了AQS,而且WriteLock和ReentrantLock一樣,使用了獨占鎖。而ReadLock和Semaphore一樣,使用了共用鎖。再往下的內容估計看過前面幾篇文章的都很熟悉了,獨占鎖通過state變數的0和1兩個狀態來控制是否有線程占有鎖,共用鎖通過state變數0或者非0來控制多個線程訪問。在上面的代碼中,ReadLock和WriteLock使用了同一個AQS,那麼在ReentrantReadWriteLock中又是怎麼控制讀鎖和寫鎖關係的呢?

1.2 ReadLock和WriteLock共用變數

讀寫鎖定義為:一個資源能夠被多個讀線程訪問,或者被一個寫線程訪問,但是不能同時存在讀寫線程。

通過這句話很容易聯想到通過兩個不同的變數來控制讀寫,當獲取到讀鎖時對讀變數+1,當獲取懂啊寫變數時對寫變數+1。但AQS中並沒有為ReadLock和WriteLock添加額外的變數,還是通過一個state來實現的。那是怎麼做到讀寫分離的呢?來看看下麵這段代碼:1。但AQS中並沒有為ReadLock和WriteLock添加額外的變數,還是通過一個state來實現的。那是怎麼做到讀寫分離的呢?來看看下麵這段代碼:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

這段代碼在Sync靜態內部類中,這裡有兩個關鍵方法sharedCount和exclusiveCount,通過名字可以看出sharedCount是共用鎖的數量,exclusiveCount是獨占鎖的數量。共用鎖通過對c像右位移16位獲得,獨占鎖通過和16位的1與運算獲得。舉個例子,當獲取讀鎖的線程有3個,寫鎖的線程有1個(當然這是不可能同時有的),state就表示為0000 0000 0000 0011 0000 0000 0000 0001,高16位代表讀鎖,通過向右位移16位(c >>> SHARED_SHIFT)得倒10進位的3,通過和0000 0000 0000 0000 1111 1111 1111 1111與運算(c & EXCLUSIVE_MASK),獲得10進位的1。弄懂了著幾個方法,就明白了為什麼通過一個state實現了讀寫共用。

這當中還有一個問題,由於16位最大全1表示為65535,所以讀鎖和寫鎖最多可以獲取65535個。

1.3 WriteLock和ReentrantLock獲取鎖的區別

上面說過,WriteLock也是獨占鎖,那麼他和ReentrantLock有什麼區別呢?最大的區別就在獲取鎖時WriteLock不僅需要考慮是否有其他寫鎖占用,同時還要考慮是否有其他讀鎖,而ReentrantLock只需要考慮自身是否被占用就行了。來看下WriteLock獲取鎖的源代碼:

public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) && //嘗試獲取獨占鎖
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //獲取失敗後排隊
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {

    Thread current = Thread.currentThread();
    int c = getState();  //獲取共用變數state
    int w = exclusiveCount(c); //獲取寫鎖數量
    if (c != 0) { //有讀鎖或者寫鎖
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread()) //寫鎖為0(證明有讀鎖),或者持有寫鎖的線程不為當前線程
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);  //當前線程持有寫鎖,為重入鎖,+acquires即可
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires)) //CAS操作失敗,多線程情況下被搶占,獲取鎖失敗。CAS成功則獲取鎖成功
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

這段代碼是不是很熟悉?和ReentrantLock中獲取鎖的代碼很相似,差別在於其中調用了exclusiveCount方法來獲取是否存在寫鎖,然後通過c != 0和w == 0判斷了是否存在讀鎖。acquireQueued和addWaiter就不詳細解說了,需要瞭解的可以查看前面ReentrantLock的文章。
到這裡大家應該對ReentrantReadWriteLock和ReentrantLock的區別應該做到心裡有數了吧。

1.4 ReadLock和Semaphore獲取鎖的區別

WriteLock是獨占模式,我們比較了它和ReentrantLock獨占鎖獲取鎖的區別,這裡我們再看看ReadLock在獲取鎖上有什麼不同呢?先看下麵的源代碼:

protected final int tryAcquireShared(int unused) {

    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current) //寫鎖不等於0的情況下,驗證是否是當前寫鎖嘗試獲取讀鎖
        return -1;
    int r = sharedCount(c);  //獲取讀鎖數量
    if (!readerShouldBlock() && //讀鎖不需要阻塞
        r < MAX_COUNT &&  //讀鎖小於最大讀鎖數量
        compareAndSetState(c, c + SHARED_UNIT)) { //CAS操作嘗試設置獲取讀鎖 也就是高位加1
        if (r == 0) {  //當前線程第一個並且第一次獲取讀鎖,
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { //當前線程是第一次獲取讀鎖的線程
            firstReaderHoldCount++;
        } else { // 當前線程不是第一個獲取讀鎖的線程,放入線程本地變數
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}ß

在上面的代碼中嘗試獲取讀鎖的過程和獲取寫鎖的過程也很相似,不同在於讀鎖只要沒有寫鎖占用並且不超過最大獲取數量都可以嘗試獲取讀鎖,而寫鎖不僅需要考慮讀鎖是否占用,也要考慮寫鎖是否占用。上面的代碼中firstReader,firstReaderHoldCount以及cachedHoldCounter都是為readHolds(ThreadLocalHoldCounter)服務的,用來記錄每個讀鎖獲取線程的獲取次數,方便獲取當前線程持有鎖的次數信息。在ThreadLocal基礎上添加了一個Int變數來統計次數,可以通過他們的實現來理解:

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> { //ThreadLocal變數ß
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

static final class HoldCounter {
    int count = 0;           //當前線程持有鎖的次數
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());   //當前線程ID
}

2. 你能用ReentrantReadWriteLock實現一個簡單的緩存嗎?

先來分析一個簡單緩存需要滿足的功能,這裡我們為了實現簡單,不考慮緩存過期策略等複雜因素。

  • 緩存主要提供兩個功能:讀和寫。
  • 讀時如果緩存中存在數據,則立即返回數據。
  • 讀時如果緩存中不存在數據,則需要從其他途徑獲取數據,同時寫入緩存。
  • 在寫入緩存的同時,為了避免其他線程同時獲取這個緩存中不存在的數據,需要阻塞其他讀線程。
    下麵我們就來通過ReentrantReadWriteLock實現上述功能:
public static void ReentrantReadWriteLockCacheSystem() {

    //這裡為了實現簡單,將緩存大小設置為4。
    Map<String, String> cacheMap = new HashMap<>(4); 
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    for (int i = 0; i < 20; i++) { //同時開啟20個線程訪問緩存

        final String key = String.valueOf(i % 4); 
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    //①讀取緩存時獲取讀鎖
                    readWriteLock.readLock().lock();  
                    //獲取讀鎖後通過key獲取緩存中的值
                    String valueStr = cacheMap.get(key); 
                    //緩存值不存在
                    if (valueStr == null) { 
                        //③釋放讀鎖後再嘗試獲取寫鎖
                        readWriteLock.readLock().unlock();  
                        try {
                            //④獲取寫鎖來寫入不存在的key值,
                            readWriteLock.writeLock().lock();  
                            valueStr = cacheMap.get(key);
                            if (valueStr == null) {
                                valueStr = key + " --- value";
                                cacheMap.put(key, valueStr); //寫入值
                                System.out.println(Thread.currentThread().getName() + " --------- put " + valueStr);
                            }
                            // ⑥鎖降級,避免被其他寫線程搶占後再次更新值,保證這一次操作的原子性
                            readWriteLock.readLock().lock(); 
                            System.out.println(Thread.currentThread().getName() + " --------- get new " + valueStr);
                        } finally {
                            readWriteLock.writeLock().unlock(); //⑤釋放寫鎖
                        }
                        
                    } else {
                        System.out.println(Thread.currentThread().getName() + " ------ get cache value");
                    }
                } finally {
                    readWriteLock.readLock().unlock();  //②釋放讀鎖
                }
            }
        }, String.valueOf(i));
        thread.start();
    }
}

首先線程會嘗試去獲取數據,需要獲取讀鎖①,如果存在值,則直接讀取並釋放讀鎖②。如果不存在值,則首先釋放已經獲取的讀鎖③,然後嘗試獲取寫鎖④。獲取到寫鎖之後,再次檢查值,因為此時可能存在其他寫鎖已經更新值,這時只需要讀取,然後釋放寫鎖⑤。如果還是沒有值,則通過其他途徑獲取值並更新然後獲取讀鎖⑥,這一步鎖降級操作是為了直接搶占讀鎖,避免釋放寫鎖之後再次獲取讀鎖時被其他寫線程搶占,這樣保證了這一次讀取數據的原子性。之後再執行⑤釋放寫鎖和②釋放讀鎖。

執行後輸出結果如下,每次執行可能輸出不同:

//1 --------- put 1 --- value
//1 --------- get new 1 --- value
//0 --------- put 0 --- value
//0 --------- get new 0 --- value
//9 ------ get cache value
//4 ------ get cache value
//2 --------- put 2 --- value
//2 --------- get new 2 --- value
//11 --------- put 3 --- value
//11 --------- get new 3 --- value
//5 ------ get cache value
//13 ------ get cache value
//6 ------ get cache value
//8 ------ get cache value
//7 ------ get cache value
//3 --------- get new 3 --- value
//10 ------ get cache value
//12 ------ get cache value
//14 ------ get cache value
//15 ------ get cache value
//16 ------ get cache value
//17 ------ get cache value
//18 ------ get cache value
//19 ------ get cache value

3. 你能自己實現一個簡單的讀寫鎖嗎?

經過上面對讀寫鎖原理的初步分析和使用,現在你能自己實現一個簡單的讀寫鎖了嗎?這裡列出了一步步實現一個簡單的讀寫鎖的過程,你可以按這個步驟自己先實現一遍。

  • 1 定義一個讀寫鎖共用變數state
  • 2 state高16位為讀鎖數量,低16位為寫鎖數量。儘量模擬ReentrantReadWriteLock的實現
  • 3 獲取讀鎖時先判斷是否有寫鎖,有則等待,沒有則將讀鎖數量加1
  • 4 釋放讀鎖數量減1,通知所有等待線程
  • 5 獲取寫鎖時需要判斷讀鎖和寫鎖是否都存在,有則等待,沒有則將寫鎖數量加1
  • 6 釋放寫鎖數量減1,通知所有等待線程
    我給出的實現代碼如下:
public class MyReadWriteLock {

    private int state = 0; //1. 定義一個讀寫鎖共用變數state
    
    //2. state高16位為讀鎖數量
    private int GetReadCount() { 
        return state >>> 16;
    }
    
    //2. 低16位為寫鎖數量
    private int GetWriteCount() { 
        return state & ((1 << 16) - 1);
    }
    
    //3. 獲取讀鎖時先判斷是否有寫鎖,有則等待,沒有則將讀鎖數量加1
    public synchronized void lockRead() throws InterruptedException{  
        
        while (GetWriteCount() > 0) {
            wait();
        }
        
        System.out.println("lockRead ---" + Thread.currentThread().getName()); 
        state = state + (1 << 16);
    }
    
    //4. 釋放讀鎖數量減1,通知所有等待線程
    public synchronized void unlockRead() {  
        state = state - ((1 << 16));
        notifyAll();
    }
    
    //5. 獲取寫鎖時需要判斷讀鎖和寫鎖是否都存在,有則等待,沒有則將寫鎖數量加1
    public synchronized void lockWriters() throws InterruptedException{  
        
        while (GetReadCount() > 0 || GetWriteCount() > 0) {
            wait();
        }
        System.out.println("lockWriters ---" + Thread.currentThread().getName());
        state++;
    }
    
    //6. 釋放寫鎖數量減1,通知所有等待線程
    public synchronized void unlockWriters(){  
        
        state--;
        notifyAll();
    }
}

4. 讀寫鎖會發生寫饑餓的情況嗎?如果發生,有沒有比較好的解決辦法?

在讀寫的過程中,寫操作一般是優先的,不能因為讀操作過多,寫操作一直等待的狀況發生,這樣會導致數據一直得不到更新,發生寫饑餓。現在大家思考一下上面我們實現的簡單讀寫鎖,是否能做到這點呢?答案很明顯,在讀寫線程都wait的情況下,通過notifyAll並不能保證寫優先執行。那在這個例子中怎麼改進這一點呢?

這裡我通過添加一個中間變數來達到這個目的,這個中間變數在獲取寫鎖之前先記錄一個寫請求,這樣一旦notifyAll,優先檢查是否有寫請求,如果有,則讓寫操作優先執行,具體代碼如下:

public class MyReadWriteLock {

    private int state = 0; //1. 定義一個讀寫鎖共用變數state
    private int writeRequest = 0; //記錄寫請求數量
    
    //2. state高16位為讀鎖數量
    private int GetReadCount() { 
        return state >>> 16;
    }
    
    //2. 低16位為寫鎖數量
    private int GetWriteCount() { 
        return state & ((1 << 16) - 1);
    }
    
    //3. 獲取讀鎖時先判斷是否有寫鎖,有則等待,沒有則將讀鎖數量加1
    public synchronized void lockRead() throws InterruptedException{  
        //寫鎖數量大於0或者寫請求數量大於0的情況下都優先執行寫
        while (GetWriteCount() > 0 || writeRequest > 0) { 
            wait();
        }
        
        System.out.println("lockRead ---" + Thread.currentThread().getName()); 
        state = state + (1 << 16);
    }
    
    //4. 釋放讀鎖數量減1,通知所有等待線程
    public synchronized void unlockRead() {  
        state = state - ((1 << 16));
        notifyAll();
    }
    
    //5. 獲取寫鎖時需要判斷讀鎖和寫鎖是否都存在,有則等待,沒有則將寫鎖數量加1
    public synchronized void lockWriters() throws InterruptedException{  
        
        writeRequest++; //寫請求+1
        while (GetReadCount() > 0 || GetWriteCount() > 0) {
            wait();
        }
        writeRequest--; //獲取寫鎖後寫請求-1
        System.out.println("lockWriters ---" + Thread.currentThread().getName());
        state++;
    }
    
    //6. 釋放寫鎖數量減1,通知所有等待線程
    public synchronized void unlockWriters(){  
        
        state--;
        notifyAll();
    }
}

大家可以測試下上面的代碼看看是不是寫請求都優先執行了呢?現在我們把這個問題放到ReentrantReadWriteLock中來考慮,顯而易見,ReentrantReadWriteLock也會發生寫請求饑餓的情況,因為寫請求一樣會排隊,不管是公平鎖還是非公平鎖,在有讀鎖的情況下,都不能保證寫鎖一定能獲取到,這樣只要讀鎖一直占用,就會發生寫饑餓的情況。那麼JDK就沒有提供什麼好辦法來解決這個問題嗎?當然是有的,那就是JDK8中新增的改進讀寫鎖---StampedLock,在下一篇文章中將會對StampedLock進行詳細講解。


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

-Advertisement-
Play Games
更多相關文章
  • 博客園首頁是需要分享乾貨的地方,今天早上寫的《HRMS(人力資源管理系統)-從單機應用到SaaS應用-系統介紹》內容下架了,所以我就按照相關規定,只分享乾貨,我把之前寫完的架構設計相關知識的內容整理髮布上來。這次主要分享一下在架構設計過程中涉及的基礎知識,主要是涵蓋系統架構方法、架構模式及設計模式,... ...
  • 上周發佈的《2018,全新出發(全力推動實現住有所居》文章,其中記錄了個人在這5年過程中的成長和收穫,有幸認識了不少博客園的朋友,大家一起學習交流,在這個過程當中好多朋友提出SaaS系統如何設計,架構方面如何下手,在這5年的過程中我參與規劃設計了很多的SaaS系統其中有不少的坑和痛苦的經驗,特別是在... ...
  • RabbitMQ實戰教程(一) : 安裝及相關概念介紹 由於本人只在 安裝 服務 ,其他系統安裝暫時沒有涉及,如果有需要請自行搜索安裝教程. . . 1 . Windows 安裝 安裝需要先安裝 ,再安裝 第一步:安裝 ,由於 是用 編寫的,所以在安裝 之前要先安裝 下載地址: 下載最新版本即可,例 ...
  • 前言 此系列是針對springboot的啟動,旨在於和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的源碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的源碼,不知道springboot在啟動過程中做了些 ...
  • 元素在順序容器中的順序與其加入容器時的位置相對應。關聯容器中元素的位置由元素相關聯的關鍵字值決定。所有容器類都共用公共的介面,不同容器按不同方式對其進行擴展。 一個容器就是一些特定類型對象的集合。順序容器為程式員提供了控制元素存儲和訪問順序的能力。 1. 順序容器概述 容器的兩種性能: 向容器中添加 ...
  • 一、SpringMVC啟動過程 Spring的MVC是基於Servlet功能實現的,每個web工程中都有一個web.xml文件,web容器在啟動的時候會載入這個配置文件,當一個web應用載入到web容器中後,在web應用開始響應客戶端的請求之前,要按照順序執行下麵幾個步驟: 1、實例化部署描述符中的 ...
  • 通用的(泛型)演算法 generic algorithm 總覽 特性: 1,標準庫的順序容器定義了很少的操作,比如添加,刪除等。 2,問題:其實還有很多操作,比如排序,查找特定的元素,替換或刪除一個特定值等,但是標準庫並未給每個容器都定義成員函數來實現這些操作。 3,解決辦法:因為演算法是相同的邏輯,只 ...
  • 做運維的朋友應該知道,公司IDC機房經常有上架、下架、報修和報廢的伺服器。如果伺服器數量很多的時候很容易造成監控遺漏。 大的互聯網公司把監控系統和CMDB(資產管理系統|配置管理資料庫系統)集成在一起,當上架一臺新機器的時候CMDB裡面會記錄相關的信息,Zabbix根據CMDB裡面信息自動Link相 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...