Java進階(三)多線程開發關鍵技術

来源:http://www.cnblogs.com/jasongj/archive/2016/06/20/5599463.html
-Advertisement-
Play Games

本文將介紹Java多線程開發必不可少的鎖和同步機制,同時介紹sleep和wait等常用的暫停線程執行的方法,並詳述synchronized的幾種使用方式,以及Java中的重入鎖(ReentrantLock)和讀寫鎖(ReadWriteLock),之後結合實例分析了重入鎖條件變數(Condition)... ...


原創文章,同步發自作者個人博客,轉載請務必以超鏈接形式在文章開頭處註明出處http://www.jasongj.com/java/multi_thread/

sleep和wait到底什麼區別

其實這個問題應該這麼問——sleep和wait有什麼相同點。因為這兩個方法除了都能讓當前線程暫停執行完,幾乎沒有其它相同點。

wait方法是Object類的方法,這意味著所有的Java類都可以調用該方法。sleep方法是Thread類的靜態方法。

wait是在當前線程持有wait對象鎖的情況下,暫時放棄鎖,並讓出CPU資源,並積極等待其它線程調用同一對象的notify或者notifyAll方法。註意,即使只有一個線程在等待,並且有其它線程調用了notify或者notifyAll方法,等待的線程只是被激活,但是它必須得再次獲得鎖才能繼續往下執行。換言之,即使notify被調用,但只要鎖沒有被釋放,原等待線程因為未獲得鎖仍然無法繼續執行。測試代碼如下所示

import java.util.Date;

public class Wait {

  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Wait.class.wait();
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Wait.class.notify();
          // Don't use sleep method to avoid confusing
          for(long i = 0; i < 200000; i++) {
            for(long j = 0; j < 100000; j++) {}
          }
          System.out.println(new Date() + " Thread2 release lock");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
      System.out.println(new Date() + " Thread2 ended");
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}

執行結果如下

Tue Jun 14 22:51:11 CST 2016 Thread1 is running
Tue Jun 14 22:51:23 CST 2016 Thread2 is running
Tue Jun 14 22:51:36 CST 2016 Thread2 release lock
Tue Jun 14 22:51:36 CST 2016 Thread1 ended
Tue Jun 14 22:51:49 CST 2016 Thread2 ended

從運行結果可以看出

  • thread1執行wait後,暫停執行
  • thread2執行notify後,thread1並沒有繼續執行,因為此時thread2尚未釋放鎖,thread1因為得不到鎖而不能繼續執行
  • thread2執行完synchronized語句塊後釋放鎖,thread1得到通知並獲得鎖,進而繼續執行

註意:wait方法需要釋放鎖,前提條件是它已經持有鎖。所以wait和notify(或者notifyAll)方法都必須被包裹在synchronized語句塊中,並且synchronized後鎖的對象應該與調用wait方法的對象一樣。否則拋出IllegalMonitorStateException

sleep方法只是讓CPU休息,並不讓出CPU資源,同時也並不釋放鎖(如果當前已經持有鎖)。實際上,調用sleep方法時並不要求持有任何鎖。

package com.test.thread;

import java.util.Date;

public class Sleep {

  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread2 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}

執行結果如下

Thu Jun 16 19:46:06 CST 2016 Thread1 is running
Thu Jun 16 19:46:08 CST 2016 Thread1 ended
Thu Jun 16 19:46:13 CST 2016 Thread2 is running
Thu Jun 16 19:46:15 CST 2016 Thread2 ended

由於thread 1和thread 2的run方法實現都在同步塊中,無論哪個線程先拿到鎖,執行sleep時並不釋放鎖,因此其它線程無法執行。直到前面的線程sleep結束並退出同步塊(釋放鎖),另一個線程才得到鎖並執行。

註意:sleep方法並不需要持有任何形式的鎖,也就不需要包裹在synchronized中。

synchronized幾種用法

每個Java對象都可以用做一個實現同步的互斥鎖,這些鎖被稱為內置鎖。線程進入同步代碼塊或方法時自動獲得內置鎖,退出同步代碼塊或方法時自動釋放該內置鎖。進入同步代碼塊或者同步方法是獲得內置鎖的唯一途徑。

實例同步方法

synchronized用於修飾實例方法(非靜態方法)時,執行該方法需要獲得的是該類實例對象的內置鎖(同一個類的不同實例擁有不同的內置鎖)。如果多個實例方法都被synchronized修飾,則當多個線程調用同一實例的不同同步方法(或者同一方法)時,需要競爭鎖。但當調用的是不同實例的方法時,並不需要競爭鎖。

靜態同步方法

synchronized用於修飾靜態方法時,執行該方法需要獲得的是該類的class對象的內置鎖(一個類只有唯一一個class對象)。調用同一個類的不同靜態同步方法時會產生鎖競爭。

同步代碼塊

synchronized用於修飾代碼塊時,進入同步代碼塊需要獲得synchronized關鍵字後面括弧內的對象(可以是實例對象也可以是class對象)的內置鎖。

synchronized使用總結

鎖的使用是為了操作臨界資源的正確性,而往往一個方法中並非所有的代碼都操作臨界資源。換句話說,方法中的代碼往往並不都需要同步。此時建議不使用同步方法,而使用同步代碼塊,只對操作臨界資源的代碼,也即需要同步的代碼加鎖。這樣做的好處是,當一個線程在執行同步代碼塊時,其它線程仍然可以執行該方法內同步代碼塊以外的部分,充分發揮多線程併發的優勢,從而相較於同步整個方法而言提升性能。

釋放Java內置鎖的唯一方式是synchronized方法或者代碼塊執行結束。若某一線程在synchronized方法或代碼塊內發生死鎖,則對應的內置鎖無法釋放,其它線程也無法獲取該內置鎖(即進入跟該內置鎖相關的synchronized方法或者代碼塊)。

Java中的鎖

重入鎖

Java中的重入鎖(即ReentrantLock)與Java內置鎖一樣,是一種排它鎖。使用synchronized的地方一定可以用ReentrantLock代替。

重入鎖需要顯示請求獲取鎖,並顯示釋放鎖。為了避免獲得鎖後,沒有釋放鎖,而造成其它線程無法獲得鎖而造成死鎖,一般建議將釋放鎖操作放在finally塊里,如下所示。

try{
  renentrantLock.lock();
  // 用戶操作
} finally {
  renentrantLock.unlock();
}

如果重入鎖已經被其它線程持有,則當前線程的lock操作會被阻塞。除了lock()方法之外,重入鎖(或者說鎖介面)還提供了其它獲取鎖的方法以實現不同的效果。

  • lockInterruptibly() 該方法嘗試獲取鎖,若獲取成功立即返回;若獲取不成功則阻塞等待。與lock方法不同的是,在阻塞期間,如果當前線程被打斷(interrupt)則該方法拋出InterruptedException。該方法提供了一種解除死鎖的途徑。
  • tryLock() 該方法試圖獲取鎖,若該鎖當前可用,則該方法立即獲得鎖並立即返回true;若鎖當前不可用,則立即返回false。該方法不會阻塞,並提供給用戶對於成功獲利鎖與獲取鎖失敗進行不同操作的可能性。
  • tryLock(long time, TimeUnit unit) 該方法試圖獲得鎖,若該鎖當前可用,則立即獲得鎖並立即返回true。若鎖當前不可用,則等待相應的時間(由該方法的兩個參數決定):1)若該時間內鎖可用,則獲得鎖,並返回true;2)若等待期間當前線程被打斷,則拋出InterruptedException;3)若等待時間結束仍未獲得鎖,則返回false。

重入鎖可定義為公平鎖或非公平鎖,預設實現為非公平鎖。

  • 公平鎖是指多個線程獲取鎖被阻塞的情況下,鎖變為可用時,最新申請鎖的線程獲得鎖。可通過在重入鎖(RenentrantLock)的構造方法中傳入true構建公平鎖,如Lock lock = new RenentrantLock(true)
  • 非公平鎖是指多個線程等待鎖的情況下,鎖變為可用狀態時,哪個線程獲得鎖是隨機的。synchonized相當於非公平鎖。可通過在重入鎖的構造方法中傳入false或者使用無參構造方法構建非公平鎖。

讀寫鎖

如上文《Java進階(二)當我們說線程安全時,到底在說什麼》所述,鎖可以保證原子性和可見性。而原子性更多是針對寫操作而言。對於讀多寫少的場景,一個讀操作無須阻塞其它讀操作,只需要保證讀和寫或者寫與寫不同時發生即可。此時,如果使用重入鎖(即排它鎖),對性能影響較大。Java中的讀寫鎖(ReadWriteLock)就是為這種讀多寫少的場景而創造的。

實際上,ReadWriteLock介面並非繼承自Lock介面,ReentrantReadWriteLock也只實現了ReadWriteLock介面而未實現Lock介面。ReentrantReadWriteLock的子類(ReadLock和WriteLock,是ReentrantReadWriteLock類的靜態內部類)實現了Lock介面。

一個ReentrantReadWriteLock實例包含一個ReentrantReadWriteLock.ReadLock實例和一個ReentrantReadWriteLock.WriteLock實例。通過readLock()writeLock()方法可分別獲得讀鎖實例和寫鎖實例,並通過Lock介面提供的獲取鎖方法獲得對應的鎖。

讀寫鎖的鎖定規則如下:

  • 獲得讀鎖後,其它線程可獲得讀鎖而不能獲取寫鎖
  • 獲得寫鎖後,其它線程即不能獲得讀鎖也不能獲得寫鎖
package com.test.thread;

import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

  public static void main(String[] args) {
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 1 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 1 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();

    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 2 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();

    new Thread(() -> {
      Lock lock = readWriteLock.writeLock();
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 3 started with write lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 3 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}

執行結果如下

Sat Jun 18 21:33:46 CST 2016  Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016  Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016  Thread 2 ended
Sat Jun 18 21:33:48 CST 2016  Thread 1 ended
Sat Jun 18 21:33:48 CST 2016  Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016  Thread 3 ended

從上面的執行結果可見,thread 1和thread 2都只需獲得讀鎖,因此它們可以並行執行。而thread 3因為需要獲取寫鎖,必須等到thread 1和thread 2釋放鎖後才能獲得鎖。

條件鎖

條件鎖只是一個幫助用戶理念的概念,實際上並沒有條件鎖這種鎖。對於每個重入鎖,都可以通過newCondition()方法綁定若幹個條件對象。

條件對象提供以下方法以實現不同的等待語義

  • await() 調用該方法的前提是,當前線程已經成功獲得與該條件對象綁定的重入鎖,否則調用該方法時會拋出IllegalMonitorStateException。調用該方法外,當前線程會釋放當前已經獲得的鎖(這一點與上文講述的Java內置鎖的wait方法一致),並且等待其它線程調用該條件對象的signal()或者signalAll()方法(這一點與Java內置鎖wait後等待notify()notifyAll()很像)。或者在等待期間,當前線程被打斷,則wait()方法會拋出InterruptedException並清除當前線程的打斷狀態。
  • await(long time, TimeUnit unit) 適用條件和行為與await()基本一致,唯一不同點在於,指定時間之內沒有收到signal()signalALL()信號或者線程中斷時該方法會返回false;其它情況返回true。
  • awaitNanos(long nanosTimeout) 調用該方法的前提是,當前線程已經成功獲得與該條件對象綁定的重入鎖,否則調用該方法時會拋出IllegalMonitorStateExceptionnanosTimeout指定該方法等待���號的的最大時間(單位為納秒)。若指定時間內收到signal()signalALL()則返回nanosTimeout減去已經等待的時間;若指定時間內有其它線程中斷該線程,則拋出InterruptedException並清除當前線程的打斷狀態;若指定時間內未收到通知,則返回0或負數。
  • awaitUninterruptibly() 調用該方法的前提是,當前線程已經成功獲得與該條件對象綁定的重入鎖,否則調用該方法時會拋出IllegalMonitorStateException。調用該方法後,結束等待的唯一方法是其它線程調用該條件對象的signal()signalALL()方法。等待過程中如果當前線程被中斷,該方法仍然會繼續等待,同時保留該線程的中斷狀態。
  • awaitUntil(Date deadline) 適用條件與行為與awaitNanos(long nanosTimeout)完全一樣,唯一不同點在於它不是等待指定時間,而是等待由參數指定的某一時刻。

調用條件等待的註意事項

  • 調用上述任意條件等待方法的前提都是當前線程已經獲得與該條件對象對應的重入鎖。
  • 調用條件等待後,當前線程讓出CPU資源。
  • 上述等待方法結束後,方法返回的前提是它能重新獲得與該條件對象對應的重入鎖。如果無法獲得鎖,仍然會繼續等待。這也是awaitNanos(long nanosTimeout)可能會返回負值的原因。
  • 一旦條件等待方法返回,則當前線程肯定已經獲得了對應的重入鎖。
  • 重入鎖可以創建若幹個條件對象,signal()signalAll()方法只能喚醒相同條件對象的等待。
  • 一個重入鎖上可以生成多個條件變數,不同線程可以等待不同的條件,從而實現更加細粒度的的線程間通信。

signal()signalAll()

  • signal() 若有一個或若幹個線程在等待該條件變數,則該方法會喚醒其中的一個(具體哪一個,無法預測)。調用該方法的前提是當前線程持有該條件變數對應的鎖,否則拋出IllegalMonitorStateException
  • signalALL() 若有一個或若幹個線程在等待該條件變數,則該方法會喚醒所有等待。調用該方法的前提是當前線程持有該條件變數對應的鎖,否則拋出IllegalMonitorStateException
package com.test.thread;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {

  public static void main(String[] args) throws InterruptedException {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 1 is waiting");
        try {
          long waitTime = condition.awaitNanos(TimeUnit.SECONDS.toNanos(2));
          System.out.println(new Date() + "\tThread 1 remaining time " + waitTime);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 1 is waken up");
      } finally {
        lock.unlock();
      }
    }).start();
    
    new Thread(() -> {
      lock.lock();
      try{
        System.out.println(new Date() + "\tThread 2 is running");
        try {
          Thread.sleep(4000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        condition.signal();
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}

執行結果如下

Sun Jun 19 15:59:09 CST 2016  Thread 1 is waiting
Sun Jun 19 15:59:09 CST 2016  Thread 2 is running
Sun Jun 19 15:59:13 CST 2016  Thread 2 ended
Sun Jun 19 15:59:13 CST 2016  Thread 1 remaining time -2003467560
Sun Jun 19 15:59:13 CST 2016  Thread 1 is waken up

從執行結果可以看出,雖然thread 2一開始就調用了signal()方法去喚醒thread 1,但是因為thread 2在4秒鐘後才釋放鎖,也即thread 1在4秒後才獲得鎖,所以thread 1的await方法在4秒鐘後才返回,並且返回負值。

信號量Semaphore

信號量維護一個許可集,可通過acquire()獲取許可(若無可用許可則阻塞),通過release()釋放許可,從而可能喚醒一個阻塞等待許可的線程。

與互斥鎖類似,信號量限制了同一時間訪問臨界資源的線程的個數,並且信號量也分公平信號量與非公平信號量。而不同的是,互斥鎖保證同一時間只會有一個線程訪問臨界資源,而信號量可以允許同一時間多個線程訪問特定資源。所以信號量並不能保證原子性。

信號量的一個典型使用場景是限制系統訪問量。每個請求進來後,處理之前都通過acquire獲取許可,若獲取許可成功則處理該請求,若獲取失敗則等待處理或者直接不處理該請求。

信號量的使用方法

  • acquire(int permits) 申請permits(必須為非負數)個許可,若獲取成功,則該方法返回並且當前可用許可數減permits;若當前可用許可數少於permits指定的個數,則繼續等待可用許可數大於等於permits;若等待過程中當前線程被中斷,則拋出InterruptedException
  • acquire() 等價於acquire(1)
  • acquireUninterruptibly(int permits) 申請permits(必須為非負數)個許可,若獲取成功,則該方法返回並且當前可用許可數減permits;若當前許可數少於permits,則繼續等待可用許可數大於等於permits;若等待過程中當前線程被中斷,繼續等待可用許可數大於等於permits,並且獲取成���後設置線程中斷狀態。
  • acquireUninterruptibly() 等價於acquireUninterruptibly(1)
  • drainPermits() 獲取所有可用許可,並返回獲取到的許可個數,該方法不阻塞。
  • tryAcquire(int permits) 嘗試獲取permits個可用許可,如果當前許可個數大於等於permits,則返回true並且可用許可數減permits;否則返回false並且可用許可數不變。
  • tryAcquire() 等價於tryAcquire(1)
  • tryAcquire(int permits, long timeout, TimeUnit unit) 嘗試獲取permits(必須為非負數)個許可,若在指定時間內獲取成功則返回true並且可用許可數減permits;若指定時間內當前線程被中斷,則拋出InterruptedException;若指定時間內可用許可數均小於permits,則返回false。
  • tryAcquire(long timeout, TimeUnit unit) 等價於tryAcquire(1, long timeout, TimeUnit unit)*
  • release(int permits) 釋放permits個許可,該方法不阻塞並且某線程調用release方法前並不需要先調用acquire方法。
  • release() 等價於release(1)

註意:與wait/notify和await/signal不同,acquire/release完全與鎖無關,因此acquire等待過程中,可用許可滿足要求時acquire可立即返回,而不用像鎖的wait和條件變數的await那樣重新獲取鎖才能返回。或者可以理解成,只要可用許可滿足需求,就已經獲得了鎖。

Java進階系列


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

-Advertisement-
Play Games
更多相關文章
  • 現在我在debug python程式就只是簡單在有可能錯誤的地方print出來看一下,不知道python有沒像c++的一些IDE一樣有單步調試這類的工具?或者說各位python大神一般是怎麼debug自己的python程式的? 應該用過 IPython 吧?想象一下,拋出異常時自動把你帶到 IPyt ...
  • 前言 Swift提供了類似C語言的流程式控制制結構,包括可以多次執行任務的for和while迴圈。還有基於特定條件選擇執行不同代碼分支的if、guard和switch語句,還有控制流程跳轉到其他代碼的break和continue語句。 Swift增加了for-in迴圈,用來更簡單地遍曆數組、字典、區間、 ...
  • 習題集解析部分 第7章 圖 ——《數據結構題集》-嚴蔚敏.吳偉民版 源碼使用說明 鏈接☛☛☛ 《數據結構-C語言版》(嚴蔚敏,吳偉民版)課本源碼+習題集解析使用說明 課本源碼合輯 鏈接☛☛☛ 《數據結構》課本源碼合輯 習題集全解析 鏈接☛☛☛ 《數據結構題集》習題解析合輯 相關測試數據下載 鏈接☛ ...
  • AVL樹是高度平衡的二叉樹,任何節點的兩個子樹的高度差別<=1 實現AVL樹 定義一個AVL樹,AVLTree,定義AVLTree的節點內部類AVLNode,節點包含以下特性: 1.key——關鍵字,對AVL樹的節點進行排序 2.left——左子樹 3.right——右子樹 4.height——高度 ...
  • 補充說明: 當前環境是在windows環境下 python版本是:python 3.4. 剛開始學習python,一邊看書一邊論壇里閱讀感興趣的代碼, http://www.oschina.net/code/snippet_1406266_43470 的代碼運行報錯情況及解決方案。 1. Impor ...
  • 1 在windows下運行python程式 1)從DOS命令行運行python腳本 用python解釋器來執行python腳本,在windows下麵python解釋器是python.exe,我的python文件安裝在D:\Python27,在這個目錄下可以看到python解釋器。完整的命令應該是: ...
  • C++引用的學習: 通常引用第一個作用,人們會想到的是引用的變數的別名;(可以說是直接操作這個變數); 引用的聲明: Type + & + name(可以認為是一個常指針) 註意:(1)&是起標識符的作用; (2)聲明引用時,必須先將其進行初始化; (3)不能建立數組的引用,因為數組是因為由若幹個元 ...
  • shell變數的賦值、變數的取值、變數的取消與查看、局部變數/用戶變數、全局變數/環境變數、特殊變數 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...