來一波騷操作,Java記憶體模型

来源:https://www.cnblogs.com/jiagooushi/archive/2023/02/21/17141168.html
-Advertisement-
Play Games

文章整理自 博學谷狂野架構師 什麼是JMM 併發編程領域的關鍵問題 線程之間的通信 線程的通信是指線程之間以何種機制來交換信息。在編程中,線程之間的通信機制有兩種,共用記憶體和消息傳遞。 ​ 在共用記憶體的併發模型里,線程之間共用程式的公共狀態,線程之間通過寫-讀記憶體中的公共狀態來隱式進行通信,典型的共 ...


文章整理自 博學谷狂野架構師

什麼是JMM

img

併發編程領域的關鍵問題

線程之間的通信

線程的通信是指線程之間以何種機制來交換信息。在編程中,線程之間的通信機制有兩種,共用記憶體和消息傳遞。
​ 在共用記憶體的併發模型里,線程之間共用程式的公共狀態,線程之間通過寫-讀記憶體中的公共狀態來隱式進行通信,典型的共用記憶體通信方式就是通過共用對象進行通信。
在消息傳遞的併發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信,在java中典型的消息傳遞方式就是wait()和notify()。

線程間的同步

同步是指程式用於控制不同線程之間操作發生相對順序的機制。

在共用記憶體併發模型里,同步是顯式進行的。程式員必須顯式指定某個方法或某段代碼需要線上程之間互斥執行。
​ 在消息傳遞的併發模型里,由於消息的發送必須在消息的接收之前,因此同步是隱式進行的。

現代電腦的記憶體模型

物理電腦中的併發問題,物理機遇到的併發問題與虛擬機中的情況有不少相似之處,物理機對併發的處理方案對於虛擬機的實現也有相當大的參考意義。

其中一個重要的複雜性來源是絕大多數的運算任務都不可能只靠處理器“計算”就能完成,處理器至少要與記憶體交互,如讀取運算數據、存儲運算結果等,這個I/O操作是很難消除的(無法僅靠寄存器來完成所有運算任務)。

早期電腦中cpu和記憶體的速度是差不多的,但在現代電腦中,cpu的指令速度遠超記憶體的存取速度,由於電腦的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代電腦系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存(Cache)來作為記憶體與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回記憶體之中,這樣處理器就無須等待緩慢的記憶體讀寫了。

基於高速緩存的存儲交互很好地解決了處理器與記憶體的速度矛盾,但是也為電腦系統帶來更高的複雜度,因為它引入了一個新的問題:緩存一致性(Cache Coherence)。

在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共用同一主記憶體(MainMemory)。當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的緩存數據不一致,舉例說明變數在多個CPU之間的共用。

如果真的發生這種情況,那同步回到主記憶體時以誰的緩存數據為準呢?為瞭解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

img

該記憶體模型帶來的問題

現代的處理器使用寫緩衝區臨時保存向記憶體寫入的數據。寫緩衝區可以保證指令流水線持續運行,它可以避免由於處理器停頓下來等待向記憶體寫入數據而產生的延遲。

同時,通過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一記憶體地址的多次寫,減少對記憶體匯流排的占用。

雖然寫緩衝區有這麼多好處,但每個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對記憶體操作的執行順序產生重要的影響:處理器對記憶體的讀/寫操作的執行順序,不一定與記憶體實際發生的讀/寫操作順序一致!
​ 處理器A和處理器B按程式的順序並行執行記憶體訪問,最終可能得到x=y=0的結果。
處理器A和處理器B可以同時把共用變數寫入自己的寫緩衝區(A1,B1),然後從記憶體中讀取另一個共用變數(A2,B2),最後才把自己寫緩存區中保存的臟數據刷新到記憶體中(A3,B3)。

當以這種時序執行時,程式就可以得到x=y=0的結果。
​ 從記憶體操作實際發生的順序來看,直到處理器A執行A3來刷新自己的寫緩存區,寫操作A1才算真正執行了。雖然處理器A執行記憶體操作的順序為:A1→A2,但記憶體操作實際發生的順序卻是A2→A1。

Processor A Processor B
代碼 a=1; //A1 x=1; //A2 b=2; //B1 y=a; //B2
運行結果 初始狀態 a=b=0 處理器允許得到結果 x=y=0

img

Java記憶體模型定義

JMM定義了Java 虛擬機(JVM)在電腦記憶體(RAM)中的工作方式。JVM是整個電腦虛擬模型,所以JMM是隸屬於JVM的。

從抽象的角度來看,JMM定義了線程和主記憶體之間的抽象關係:線程之間的共用變數存儲在主記憶體(Main Memory)中,每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體中存儲了該線程以讀/寫共用變數的副本。

本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬體和編譯器優化。

img

Java記憶體區域

img

Java虛擬機在運行程式時會把其自動管理的記憶體劃分為以上幾個區域,每個區域都有的用途以及創建銷毀的時機,其中藍色部分代表的是所有線程共用的數據區域,而紫色部分代表的是每個線程的私有數據區域。

方法區

方法區屬於線程共用的記憶體區域,又稱Non-Heap(非堆),主要用於存儲已被虛擬機載入的類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據,根據Java 虛擬機規範的規定,當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError 異常。

值得註意的是在方法區中存在一個叫運行時常量池(Runtime Constant Pool)的區域,它主要用於存放編譯器生成的各種字面量和符號引用,這些內容將在類載入後存放到運行時常量池中,以便後續使用。

JVM堆

Java 堆也是屬於線程共用的記憶體區域,它在虛擬機啟動時創建,是Java 虛擬機所管理的記憶體中最大的一塊,主要用於存放對象實例,幾乎所有的對象實例都在這裡分配記憶體,註意Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做GC 堆,如果在堆中沒有記憶體完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

程式計數器

屬於線程私有的數據區域,是一小塊記憶體空間,主要代表當前線程所執行的位元組碼行號指示器。位元組碼解釋器工作時,通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

虛擬機棧

屬於線程私有的數據區域,與線程同時創建,總數與線程關聯,代表Java方法執行的記憶體模型。每個方法執行時都會創建一個棧楨來存儲方法的的變數表、操作數棧、動態鏈接方法、返回值、返回地址等信息。每個方法從調用直結束就對於一個棧楨在虛擬機棧中的入棧和出棧過程,如下(圖有誤,應該為棧楨):

本地方法棧

本地方法棧屬於線程私有的數據區域,這部分主要與虛擬機用到的 Native 方法相關,一般情況下,我們無需關心此區域。

小結

這裡之所以簡要說明這部分內容,註意是為了區別Java記憶體模型與Java記憶體區域的劃分,畢竟這兩種劃分是屬於不同層次的概念。

Java記憶體模型概述

Java記憶體模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括實例欄位,靜態欄位和構成數組對象的元素)的訪問方式。

由於JVM運行程式的實體是線程,而每個線程創建時JVM都會為其創建一個工作記憶體(有些地方稱為棧空間),用於存儲線程私有的數據,而Java記憶體模型中規定所有變數都存儲在主記憶體,主記憶體是共用記憶體區域,

所有線程都可以訪問,但線程對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體拷貝的自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,工作記憶體中存儲著主記憶體中的變數副本拷貝,

前面說過,工作記憶體是每個線程的私有數據區域,因此不同的線程間無法訪問對方的工作記憶體,線程間的通信(傳值)必須通過主記憶體來完成,其簡要訪問過程如下圖

img

需要註意的是,JMM與Java記憶體區域的劃分是不同的概念層次,更恰當說JMM描述的是一組規則,通過這組規則控製程序中各個變數在共用數據區域和私有數據區域的訪問方式,JMM是圍繞原子性,有序性、可見性展開的(稍後會分析)。

JMM與Java記憶體區域唯一相似點,都存在共用數據區域和私有數據區域,在JMM中主記憶體屬於共用數據區域,從某個程度上講應該包括了堆和方法區,而工作記憶體數據線程私有數據區域,從某個程度上講則應該包括程式計數器、虛擬機棧以及本地方法棧。

或許在某些地方,我們可能會看見主記憶體被描述為堆記憶體,工作記憶體被稱為線程棧,實際上他們表達的都是同一個含義。關於JMM中的主記憶體和工作記憶體說明如下

主記憶體

主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主記憶體中,不管該實例對象是成員變數還是方法中的本地變數(也稱局部變數),當然也包括了共用的類信息、常量、靜態變數。

由於是共用數據區域,多條線程對同一個變數進行訪問可能會發現線程安全問題。

工作記憶體

主要存儲當前方法的所有本地變數信息(工作記憶體中存儲著主記憶體中的變數副本拷貝),每個線程只能訪問自己的工作記憶體,即線程中的本地變數對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在自己的工作記憶體中創建屬於當前線程的本地變數,當然也包括了位元組碼行號指示器、相關Native方法的信息。

註意由於工作記憶體是每個線程的私有數據,線程間無法相互訪問工作記憶體,因此存儲在工作記憶體的數據不存線上程安全問題。

數據同步

弄清楚主記憶體和工作記憶體後,接瞭解一下主記憶體與工作記憶體的數據存儲類型以及操作方式,根據虛擬機規範,對於一個實例對象中的成員方法而言,如果方法中包含本地變數是基本數據類型(boolean,byte,short,char,int,long,float,double),將直接存儲在工作記憶體的幀棧結構中,但倘若本地變數是引用類型,那麼該變數的引用會存儲在功能記憶體的幀棧中,而對象實例將存儲在主記憶體(共用數據區域,堆)中。

但對於實例對象的成員變數,不管它是基本數據類型或者包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區。

至於static變數以及類本身相關信息將會存儲在主記憶體中。需要註意的是,在主記憶體中的實例對象可以被多線程共用,倘若兩個線程同時調用了同一個對象的同一個方法,那麼兩條線程會將要操作的數據拷貝一份到自己的工作記憶體中,執行完成操作後才刷新到主記憶體,簡單示意圖如下所示:

img

硬體記憶體架構與Java記憶體模型

硬體記憶體架構

img

正如上圖所示,經過簡化CPU與記憶體操作的簡易圖,實際上沒有這麼簡單,這裡為了理解方便,我們省去了南北橋並將三級緩存統一為CPU緩存(有些CPU只有二級緩存,有些CPU有三級緩存)。

就目前電腦而言,一般擁有多個CPU並且每個CPU可能存在多個核心,多核是指在一枚處理器(CPU)中集成兩個或多個完整的計算引擎(內核),這樣就可以支持多任務並行執行,從多線程的調度來說,每個線程都會映射到各個CPU核心中並行運行。

在CPU內部有一組CPU寄存器,寄存器是cpu直接訪問和處理的數據,是一個臨時放數據的空間。一般CPU都會從記憶體取數據到寄存器,然後進行處理,但由於記憶體的處理速度遠遠低於CPU,導致CPU在處理指令時往往花費很多時間在等待記憶體做準備工作

於是在寄存器和主記憶體間添加了CPU緩存,CPU緩存比較小,但訪問速度比主記憶體快得多,如果CPU總是操作主記憶體中的同一址地的數據,很容易影響CPU執行速度,此時CPU緩存就可以把從記憶體提取的數據暫時保存起來,如果寄存器要取記憶體中同一位置的數據,直接從緩存中提取,無需直接從主記憶體取。

需要註意的是,寄存器並不每次數據都可以從緩存中取得數據,萬一不是同一個記憶體地址中的數據,那寄存器還必須直接繞過緩存從記憶體中取數據。

所以並不每次都得到緩存中取數據,這種現象有個專業的名稱叫做緩存的命中率,從緩存中取就命中,不從緩存中取從記憶體中取,就沒命中,可見緩存命中率的高低也會影響CPU執行性能,這就是CPU、緩存以及主記憶體間的簡要交互過程,

總而言之當一個CPU需要訪問主存時,會先讀取一部分主存數據到CPU緩存(當然如果CPU緩存中存在需要的數據就會直接從緩存獲取),進而在讀取CPU緩存到寄存器,當CPU需要寫數據到主存時,同樣會先刷新寄存器中的數據到CPU緩存,然後再把數據刷新到主記憶體中。

Java線程與硬體處理器

瞭解完硬體的記憶體架構後,接著瞭解JVM中線程的實現原理,理解線程的實現原理,有助於我們瞭解Java記憶體模型與硬體記憶體架構的關係,在Window系統和Linux系統上,Java線程的實現是基於一對一的線程模型,所謂的一對一模型,實際上就是通過語言級別層面程式去間接調用系統內核的線程模型,即我們在使用Java線程時,Java虛擬機內部是轉而調用當前操作系統的內核線程來完成當前任務。

這裡需要瞭解一個術語,內核線程(Kernel-Level Thread,KLT),它是由操作系統內核(Kernel)支持的線程,這種線程是由操作系統內核來完成線程切換,內核通過操作調度器進而對線程執行調度,並將線程的任務映射到各個處理器上。每個內核線程可以視為內核的一個分身,這也就是操作系統可以同時處理多任務的原因。

由於我們編寫的多線程程式屬於語言層面的,程式一般不會直接去調用內核線程,取而代之的是一種輕量級的進程(Light Weight Process),也是通常意義上的線程,由於每個輕量級進程都會映射到一個內核線程,因此我們可以通過輕量級進程調用內核線程,進而由操作系統內核將任務映射到各個處理器,這種輕量級進程與內核線程間1對1的關係就稱為一對一的線程模型。如下圖

img

如圖所示,每個線程最終都會映射到CPU中進行處理,如果CPU存在多核,那麼一個CPU將可以並行執行多個線程任務。

Java記憶體模型與硬體記憶體架構的關係

通過對前面的硬體記憶體架構、Java記憶體模型以及Java多線程的實現原理的瞭解,我們應該已經意識到,多線程的執行最終都會映射到硬體處理器上進行執行,但Java記憶體模型和硬體記憶體架構並不完全一致。

對於硬體記憶體來說只有寄存器、緩存記憶體、主記憶體的概念,並沒有工作記憶體(線程私有數據區域)和主記憶體(堆記憶體)之分,也就是說Java記憶體模型對記憶體的劃分對硬體記憶體並沒有任何影響,因為JMM只是一種抽象的概念,是一組規則,並不實際存在,

不管是工作記憶體的數據還是主記憶體的數據,對於電腦硬體來說都會存儲在電腦主記憶體中,當然也有可能存儲到CPU緩存或者寄存器中,因此總體上來說,Java記憶體模型和電腦硬體記憶體架構是一個相互交叉的關係,是一種抽象概念劃分與真實物理硬體的交叉。(註意對於Java記憶體區域劃分也是同樣的道理)

img

當對象和變數可以存儲在電腦的各種不同存儲區域中時,可能會出現某些問題。兩個主要問題是:

  • 線程更新(寫入)共用變數的可見性。
  • 讀取,檢查和寫入共用變數時的競爭條件。
共用對象的可見性

如果兩個或多個線程共用一個對象,而沒有正確使用 volatile 聲明或同步,則一個線程對共用對象的更改對於在其他CPU上運行的線程是不可見的。

這樣,每個線程最終都可能擁有自己的共用對象副本,每個副本都位於不同的CPU緩存中,並且其中的內容不相同。

下圖簡單說明瞭情況。在左邊CPU上運行的一個線程將共用對象複製到其CPU緩存中,並將其 count 變數更改為2。此更改對於在CPU上運行的其他線程不可見,因為count的更新尚未刷新回主記憶體。

img

要解決此問題,您可以使用Java的volatile關鍵字。volatile 關鍵字可以確保變數從主記憶體中直接讀取而不是從緩存中,並且更新的時候總是立即寫回主記憶體。

競爭條件

如果兩個或多個線程共用一個對象,並且多個線程更新該共用對象中的成員變數,則可能會出現競爭條件

想象一下,如果線程A將共用對象的變數count讀入其CPU緩存中。再想象一下,線程B也做了同樣的事情,但是進入到了不同的CPU緩存。現線上程A添加一個值到count,線程B執行相同的操作。現在var1已經增加了兩次,每次CPU緩存一次。

如果這些增加操作按順序執行,則變數count將增加兩次並將”原始值+ 2”後產生的新值寫回主存儲器。

但是,兩個增加操作同時執行卻沒有進行適當的同步。無論線程A和B中的哪一個將其更新版本count寫回主到存儲器,更新的值將僅比原始值多1,儘管有兩個增加操作。

該圖說明瞭如上所述的競爭條件問題的發生:

img

要解決此問題,您可以使用Java synchronized塊。同步塊保證在任何給定時間只有一個線程可以進入代碼的臨界區。同步塊還保證在同步塊內訪問的所有變數都將從主存儲器中讀入,當線程退出同步塊時,所有更新的變數將再次刷新回主存儲器,無論變數是否聲明為volatile。

JMM存在的必要性

在明白了Java記憶體區域劃分、硬體記憶體架構、Java多線程的實現原理與Java記憶體模型的具體關係後,接著來談談Java記憶體模型存在的必要性。

由於JVM運行程式的實體是線程,而每個線程創建時JVM都會為其創建一個工作記憶體(有些地方稱為棧空間),用於存儲線程私有的數據,線程與主記憶體中的變數操作必須通過工作記憶體間接完成,主要過程是將變數從主記憶體拷貝的每個線程各自的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,如果存在兩個線程同時對一個主記憶體中的實例對象的變數進行操作就有可能誘發線程安全問題。

如下圖,主記憶體中存在一個共用變數x,現在有A和B兩條線程分別對該變數x=1進行操作,A/B線程各自的工作記憶體中存在共用變數副本x。

假設現在A線程想要修改x的值為2,而B線程卻想要讀取x的值,那麼B線程讀取到的值是A線程更新後的值2還是更新前的值1呢?答案是,不確定,即B線程有可能讀取到A線程更新前的值1,也有可能讀取到A線程更新後的值2,這是因為工作記憶體是每個線程私有的數據區域,而線程A變數x時,

首先是將變數從主記憶體拷貝到A線程的工作記憶體中,然後對變數進行操作,操作完成後再將變數x寫回主內,而對於B線程的也是類似的,這樣就有可能造成主記憶體與工作記憶體間數據存在一致性問題,假如A線程修改完後正在將數據寫回主記憶體,而B線程此時正在讀取主記憶體,即將x=1拷貝到自己的工作記憶體中,

這樣B線程讀取到的值就是x=1,但如果A線程已將x=2寫回主記憶體後,B線程才開始讀取的話,那麼此時B線程讀取到的就是x=2,但到底是哪種情況先發生呢?這是不確定的,這也就是所謂的線程安全問題。

img

為瞭解決類似上述的問題,JVM定義了一組規則,通過這組規則來決定一個線程對共用變數的寫入何時對另一個線程可見,這組規則也稱為Java記憶體模型(即JMM),JMM是圍繞著程式執行的原子性、有序性、可見性展開的,下麵我們看看這三個特性。

本文由傳智教育博學谷狂野架構師教研團隊發佈。

如果本文對您有幫助,歡迎關註點贊;如果您有任何建議也可留言評論私信,您的支持是我堅持創作的動力。

轉載請註明出處!


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

-Advertisement-
Play Games
更多相關文章
  • 隊列的概念 在說隊列之前,先回憶一下棧是什麼,我們一般說棧是一個先進後出的數據結構,而隊列就是先進先出的數據結構。 隊列是定在表的一端進行插入,表的另一端進行刪除。 通常,我們稱進數據的一端為隊尾,出數據的一端為隊首(這邊需要註意,經常會記反起碼我是這樣的),數據元素進隊列的過程稱為入隊,出隊列的過 ...
  • 自定義線程池 package com.appletree24; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; import java.util.concurrent.ExecutionEx ...
  • 一、前言 QPython 3c在大佬的改進下,擁有了基於sl4a的FullScreenWrapper2全屏框架。文章將用該框架製作我們的可視化應用【ONE一個】。 二、最終效果如下 三、準備工作 AIDE: 使用佈局助手生成xml佈局代碼 QPython 3C: 使用FullScreenWrappe ...
  • 這一篇主要介紹的是電商網站的統計功能,後臺使用的是Java語言,springMvc框架結合前端Jquer,前端趨勢展示組件使用的是百度開源框架Echarts,這個應該大家或多或少的都有瞭解過,下麵我結合實際項目案例給大家看下項目中是如何實現的。 一、前端頁面到百度下載趨勢圖echarts.js插件, ...
  • ##2.使用I/O復用技術和線程池 網路中有很多用戶會嘗試去connect()這個WebServer上正在listen的這個port,而監聽到的這些連接會排隊等待被accept()。由於用戶連接請求是隨機到達的非同步事件,每當監聽socket(listenfd)listen到新的客戶連接並且放入監聽隊 ...
  • 一、什麼是異常 異常就是程式運行時發生錯誤的信號(在程式出現錯誤時,則會產生一個異常,若程式沒有處理它,則會拋出該異常,程式的運行也隨之終止),在python中,錯誤觸發的異常如下 1 語法錯誤 語法錯誤,根本過不了python解釋器的語法檢測,必須在程式執行前就改正。 # 語法錯誤示範一 if # ...
  • @ResponseBody註解的作用是將controller的方法返回的對象 通過適當的轉換器 轉換為指定的格式之後,寫入到response對象的body區(響應體中),通常用來返回JSON數據或者是XML。 數據,需要註意的呢,在使用此註解之後不會再走視圖處理器,而是直接將數據寫入到輸入流中,它的 ...
  • 自從學了Python後就逼迫用Python來處理Excel,所有操作用Python實現。目的是鞏固Python,與增強數據處理能力。這也是我寫這篇文章的初衷。廢話不說了,直接進入正題。 數據是網上找到的銷售數據,長這樣: 一、關聯公式:Vlookup vlookup是excel幾乎最常用的公式,一般 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...