美團一面:說說synchronized的實現原理?問麻了。。。。

来源:https://www.cnblogs.com/coderacademy/p/18120482
-Advertisement-
Play Games

`synchronized`作為Java併發編程的基礎構建塊,其簡潔易用的語法形式背後蘊含著複雜的底層實現原理和技術細節。深入理解`synchronized`的運行機制,不僅有助於我們更好地利用這一特性編寫出高效且安全的併發程式。 ...


引言

在現代軟體開發領域,多線程併發編程已經成為提高系統性能、提升用戶體驗的重要手段。然而,多線程環境下的數據同步與資源共用問題也隨之而來,處理不當可能導致數據不一致、死鎖等各種併發問題。為此,Java語言提供了一種內置的同步機制——synchronized關鍵字,它能夠有效地解決併發控制的問題,確保共用資源在同一時間只能由一個線程訪問,從而維護程式的正確性與一致性。

synchronized作為Java併發編程的基礎構建塊,其簡潔易用的語法形式背後蘊含著複雜的底層實現原理和技術細節。深入理解synchronized的運行機制,不僅有助於我們更好地利用這一特性編寫出高效且安全的併發程式,同時也有利於我們在面對複雜併發場景時,做出更為明智的設計決策和優化策略。

本文將從synchronized的基本概念出發,逐步剖析其內在的工作機制,探討諸如監視器(Monitor)等關鍵技術點,並結合實際應用場景來展示synchronized的實際效果和最佳實踐。通過對synchronized底層實現原理的深度解讀,旨在為大家揭示Java併發世界的一隅,提升對併發編程的認知高度和實戰能力。

synchronized是什麼?

synchronized是Java中實現線程同步的關鍵字,主要用於保護共用資源的訪問,確保在多線程環境中同一時間只有一個線程能夠訪問特定的代碼段或方法。它提供了互斥性和可見性兩個重要特性,確保了線程間操作的原子性和數據的一致性。

synchronized的特性

synchronized關鍵字具有三個基本特性,分別是互斥性、可見性和有序性。

互斥性

synchronized關鍵字確保了在其控制範圍內的代碼在同一時間只能被一個線程執行,實現了資源的互斥訪問。當一個線程進入了synchronized代碼塊或方法時,其他試圖進入該同步區域的線程必須等待,直至擁有鎖的線程執行完畢並釋放鎖。

可見性

synchronized還確保了線程間的數據可見性。一旦一個線程在synchronized塊中修改了共用變數的值,其他隨後進入同步區域的線程可以看到這個更改。這是因為synchronized的解鎖過程包含了將工作記憶體中的最新值刷新回主記憶體的操作,而加鎖過程則會強制從主記憶體中重新載入變數的值。

有序性

synchronized提供的第三個特性是有序性,它可以確保在多線程環境下,對於同一個鎖的解鎖操作總是先行於隨後對同一個鎖的加鎖操作。這就意味著,通過synchronized建立起了線程之間的記憶體操作順序關係,有效地解決了由於編譯器和處理器優化可能帶來的指令重排序問題。

synchronized可以實現哪鎖?

有上述synchronized的特性,我們可以知道synchronized可以實現這些鎖:

  1. 可重入鎖(Reentrant Lock)synchronized 實現的鎖是可重入的,這意味著同一個線程可以多次獲取同一個鎖,而不會被阻塞。這種鎖機制允許線程在持有鎖的情況下再次獲取相同的鎖,避免了死鎖的發生。
  2. 排它鎖/互斥鎖/獨占鎖synchronized 實現的鎖是互斥的,也就是說,在同一時間只有一個線程能夠獲取到鎖,其他線程必須等待該線程釋放鎖才能繼續執行。這確保了同一時刻只有一個線程可以訪問被鎖定的代碼塊或方法,從而保證了數據的一致性和完整性。
  3. 悲觀鎖synchronized 實現的鎖屬於悲觀鎖,因為它預設情況下假設會發生競爭,並且會導致其他線程阻塞,直到持有鎖的線程釋放鎖。悲觀鎖的特點是對併發訪問持保守態度,認為會有其他線程來競爭共用資源,因此在訪問共用資源之前會先獲取鎖。
  4. 非公平鎖: synchronized在早期的Java版本中,預設實現的是非公平鎖,也就是說,線程獲取鎖的順序並不一定按照它們請求鎖的順序來進行,而是允許“插隊”,即已經在等待隊列中的線程可能被後來請求鎖的線程搶占。

有關Java中的鎖的分類,請參考:阿裡二面:Java中鎖的分類有哪些?你能說全嗎?

synchronized使用方式

synchronized關鍵字可以修飾方法、代碼塊或靜態方法,用於確保同一時間只有一個線程可以訪問被synchronized修飾的代碼片段。

修飾實例方法

synchronized修飾實例方法時,鎖住的是當前實例對象(this)。這意味著在同一時刻,只能有一個線程訪問此方法,所有對該對象實例的其他同步方法調用將會被阻塞,直到該線程釋放鎖。

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修飾實例方法,鎖住的是當前實例對象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedInstanceMethod sim = new SynchronizedInstanceMethod();
        Thread t1 = new Thread(sim);
        Thread t2 = new Thread(sim);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

像上述這個例子,大家在接觸多線程時一定會看過或者寫過類似的代碼,i++在多線程的情況下是線程不安全的,所以我們使用synchronized作用在累加的方法上,使其變成線程安全的。上述列印結果為:

Final block counter value: 2000

而對於synchronized作用於實例方法上時,鎖的是當前實例對象,但是如果我們鎖住的是不同的示例對象,那麼synchronized就不能保證線程安全了。如下代碼:

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修飾實例方法,鎖住的是當前實例對象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedInstanceMethod());
        Thread t2 = new Thread(new SynchronizedInstanceMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

執行結果為:

Final counter value: 1491
修飾靜態方法

synchronized修飾的是靜態方法,那麼鎖住的是類的Class對象,因此,無論多少個該類的實例存在,同一時刻也只有一個線程能夠訪問此靜態同步方法。針對修飾實例方法的線程不安全的示例,我們只需要在synchronized修飾的實例方法上加上static,將其變成靜態方法,此時synchronized鎖住的就是類的class對象。

public class SynchronizedStaticMethod implements Runnable{

    private static int counter = 0;

    // 修飾實例方法,鎖住的是當前實例對象
    private static synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedStaticMethod());
        Thread t2 = new Thread(new SynchronizedStaticMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

執行結果為:

Final counter value: 2000
修飾代碼塊

通過指定對象作為鎖,可以更精確地控制同步範圍。這種方式允許在一個方法內部對不同對象進行不同的同步控制。可以指定一個對象作為鎖,只有持有該對象鎖的線程才能執行被synchronized修飾的代碼塊。

public class SynchronizedBlock implements Runnable{

    private static int counter = 0;

    @Override
    public void run() {
        // 這個this還可以是SynchronizedBlock.class,說明鎖住的是class對象
        synchronized (this){
            for (int i = 0; i < 1000; i++) {
                counter++;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedBlock block = new SynchronizedBlock();
        Thread t1 = new Thread(block);
        Thread t2 = new Thread(block);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

synchronized 內置鎖作為一種對象級別的同步機制,其作用在於確保臨界資源的互斥訪問,實現線程安全。它本質上鎖定的是對象的監視器(Object Monitor),而非具體的引用變數。這種鎖具有可重入性,即同一個線程在已經持有某對象鎖的情況下,仍能再次獲取該對象的鎖,這顯著增強了線程安全代碼的編寫便利性,併在一定程度上有助於降低因線程交互引起的死鎖風險。

關於如何避免死鎖,請參考:阿裡二面:如何定位&避免死鎖?連著兩個面試問到了!

synchronized的底層原理

在JDK 1.6之前,synchronized關鍵字所實現的鎖機制確實被認為是重量級鎖。這是因為早期版本的Java中,synchronized的實現依賴於操作系統的互斥量(Mutexes)來實現線程間的同步,這涉及到了從用戶態到內核態的切換以及線程上下文切換等相對昂貴的操作。一旦一個線程獲得了鎖,其他試圖獲取相同鎖的線程將會被阻塞,這種阻塞操作會導致線程狀態的改變和CPU資源的消耗,因此在高併發、低鎖競爭的情況下,這種鎖機制可能會成為性能瓶頸。

而在JDK 1.6中,對synchronized進行了大量優化,其中包括引入了偏向鎖(Biased Locking)、輕量級鎖(Lightweight Locking)的概念。接下來我們先說一下JDK1.6之前synchronized的原理。

對象的組成結構

在JDK1.6之前,在Java虛擬機中,Java對象的記憶體結構主要有對象頭(Object Header)實例數據(Instance Data)對齊填充(Padding) 三個部分組成。

  1. 對象頭(Object Header)
    對象頭主要包含了兩部分信息:Mark Word(標記欄位)和指向類元數據(Class Metadata)的指針。Mark Word 包含了一些重要的標記信息,比如對象是否被鎖定、對象的哈希碼、GC相關信息等。類元數據指針指向對象的類元數據,用於確定對象的類型信息、方法信息等。

  2. 實例數據(Instance Data)
    實例數據是對象的成員變數和實例方法所占用的記憶體空間,它們按照聲明的順序依次存儲在對象的實例數據區域中。實例數據包括對象的所有非靜態成員變數和非靜態方法。

  3. 填充(Padding)
    在JDK 1.6及之前的版本中,為了保證對象在記憶體中的存儲地址是8位元組的整數倍,可能會在對象的實例數據之後添加一些填充位元組。這些填充位元組的目的是對齊記憶體地址,提高記憶體訪問效率。填充位元組通常不包含任何實際數據,只是用於占位。

JDK1.6之前對象結構.png

對象頭

在JDK 1.6之前的Java HotSpot虛擬機中,對象頭的基本組成依然包含Mark Word和類型指針(Klass Pointer),但當時對於鎖的實現還沒有引入偏向鎖和輕量級鎖的概念,因此對象頭中的Mark Word在處理鎖狀態時比較簡單,主要是用來存儲鎖的狀態信息以及與垃圾收集相關的數據。在一個32位系統重對象頭大小通常約為32位,而在64位系統中大小通常為64位。
對象頭組成部分:

  1. Mark Word(標記字)
    在早期版本的HotSpot虛擬機中,Mark Word主要存儲的信息包括:
  • 對象的hashCode(在沒有鎖定時)。
  • 對象的分代年齡(用於垃圾回收演算法)。
  • 鎖狀態信息,如無鎖、重量級鎖狀態(在使用synchronized關鍵字時)。
  • 對象的鎖指針(Monitor地址,當對象被重量級鎖鎖定時,存儲的是指向重量級鎖(Monitor)的指針)。

HotSpot虛擬機對象頭Mark Word.png

對象頭中的Mark Word是一個非固定的數據結構,它會根據對象的狀態復用自己的存儲空間,存儲不同的數據。在Java HotSpot虛擬機中,Mark Word會隨著程式運行和對象狀態的變化而存儲不同的信息。其信息變化如下:

image.png

從存儲信息的變化可以看出:

  • 對象頭的最後兩位存儲了鎖的標誌位,01表示初始狀態,即未加鎖。此時,對象頭記憶體儲的是對象自身的哈希碼。無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。
  • 當進入偏向鎖階段時,對象頭內的標誌位變為01,並且存儲當前持有鎖的線程ID。這意味著只有第一個獲取鎖的線程才能繼續持有鎖,其他線程不能競爭同一把鎖。
  • 在輕量級鎖階段,標誌位變為00,對象頭記憶體儲的是指向線程棧中鎖記錄的指針。這種情況下,多個線程可以通過比較鎖記錄的地址與對象頭內的指針地址來確定自己是否擁有鎖。

其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化後新增加的。重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。

  1. 類型指針(Klass Pointer 或 Class Pointer)
    類型指針指向對象的類元數據(Class Metadata),即對象屬於哪個類的類型信息,用於確定對象的方法表和欄位佈局等。在一個32位系統重大小通常約為32位,而在64位系統中大小通常為64位。

  2. 數組長度(Array Length)(僅對數組對象適用):
    如果對象是一個數組,對象頭中會額外包含一個欄位來存儲數組的長度。在一個32位系統中大小通常約為32位,而在64位系統中大小通常為64位。

監視器(Monitor)

在Java中,每個對象都與一個Monitor關聯,Monitor是一種同步機制,負責管理線程對共用資源的訪問許可權。當一個Monitor被線程持有時,對象便處於鎖定狀態。Java的synchronized關鍵字在JVM層面上通過MonitorEnterMonitorExit指令實現方法同步和代碼塊同步。MonitorEnter嘗試獲取對象的Monitor所有權(即獲取對象鎖),MonitorExit確保每個MonitorEnter操作都有對應的釋放操作。

在HotSpot虛擬機中,Monitor具體由ObjectMonitor實現,其結構如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //鎖計數器,表示重入次數,每當線程獲取鎖時加1,釋放時減1。
    _waiters      = 0, //等待線程總數,不一定在實際的ObjectMonitor中有直接體現,但在管理線程同步時是一個重要指標。
    _recursions   = 0; //與_count類似,表示當前持有鎖的線程對鎖的重入次數。
    _object       = NULL; // 通常指向關聯的Java對象,即當前Monitor所保護的對象。
    _owner        = NULL; // 持有ObjectMonitor對象的線程地址,即當前持有鎖的線程。
    _WaitSet      = NULL; //存儲那些調用過`wait()`方法並等待被喚醒的線程隊列。
    _WaitSetLock  = 0 ; // 用於保護_WaitSet的鎖。
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //阻塞在EntryList上的單向線程列表,可能用於表示自旋等待隊列或輕量級鎖的自旋鏈表。
    FreeNext      = NULL ; // 在對象Monitor池中可能用於鏈接空閑的ObjectMonitor對象。
    _EntryList    = NULL ; // 等待鎖的線程隊列,當線程請求鎖但發現鎖已被持有時,會被放置在此隊列中等待。
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; // 標誌位,可能用於標識_owner是否指向一個真實的線程對象。
  }

其中最重要的就是_owner_WaitSet_EntryListcount幾個欄位,他們之間的轉換關係:

  1. _owner:
    當一個線程首次成功執行synchronized代碼塊或方法時,會嘗試獲取對象的Monitor(即ObjectMonitor),並將自身設置為_owner。該線程此刻擁有了對象的鎖,可以獨占訪問受保護的資源。

  2. _EntryList_owner:
    當多個線程同時嘗試獲取鎖時,除第一個成功獲取鎖的線程外,其餘線程會進入_EntryList排隊等待。一旦_owner線程釋放鎖,_EntryList中的下一個線程將有機會獲取鎖併成為新的_owner

  3. _owner_WaitSet:
    _owner線程在持有鎖的情況下調用wait()方法時,它會釋放鎖(即_owner置為NULL),並把自己從_owner轉變為等待狀態,然後將自己添加到_WaitSet中。這時,線程進入等待狀態,暫停執行,等待其他線程通過notify()notifyAll()喚醒。

  4. _WaitSet_EntryList:
    當其他線程調用notify()notifyAll()方法時,會選擇一個或全部在_WaitSet中的線程,將它們從_WaitSet移除,並重新加入到_EntryList中。這樣,這些線程就有機會再次嘗試獲取鎖併成為新的_owner

有上述轉換關係我們可以發現,當多線程訪問同步代碼時:

  1. 線程首先嘗試進入_EntryList競爭鎖,成功獲取Monitor後,將_owner設置為當前線程並將count遞增。
  2. 若線程調用wait()方法,會釋放Monitor、清空_owner,並將線程移到_WaitSet中等待被喚醒。
  3. 當線程執行完畢或調用notify()/notifyAll()喚醒等待線程後,會釋放Monitor,使得其他線程有機會獲取鎖。

在Java對象的對象頭(Mark Word)中,存儲了與鎖相關的狀態信息,這使得任意Java對象都能作為鎖來使用,同時,notify/notifyAll/wait等方法正是基於Monitor鎖對象來實現的,因此這些方法必須在synchronized代碼塊中調用。

我們查看上述同步代碼塊SynchronizedBlock的位元組碼文件:

image.png

從上述位元組碼中可以看到同步代碼塊的實現是由monitorenter 和monitorexit指令完成的,其中monitorenter指令所在的位置是同步代碼塊開始的位置,第一個monitorexit指令是用於正常結束同步代碼塊的指令,第二個monitorexit指令是用於異常結束時所執行的釋放Monitor指令。

關於查看class文件的位元組碼文件,有兩種方式:1、通過命令: javap -verbose <class路徑>/class文件。2、IDEA中通過插件:jclasslib Bytecode viewer

我們再看一下作用於同步方法的位元組碼:

image.png

我們可以看出同步方法上沒有monitorenter 和 monitorexit 這兩個指令了,而在查看該方法的class文件的結構信息時發現了Access flags後邊的synchronized標識,該標識表明瞭該方法是一個同步方法。Java虛擬機通過該標識可以來辨別一個方法是否為同步方法,如果有該標識,線程將持有Monitor,在執行方法,最後釋放Monitor。

image.png

總結

synchronized作用於同步代碼塊時的原理:
Java虛擬機使用monitorenter和monitorexit指令實現同步塊的同步。monitorenter指令在進入同步代碼塊時執行,嘗試獲取對象的Monitor(即鎖),monitorexit指令在退出同步代碼塊時執行,釋放Monitor。

而對於方法級別的同步的原理:
Java虛擬機通過在方法的訪問標誌(Access flags)中設置ACC_SYNCHRONIZED標誌來實現方法同步。當一個方法被聲明為synchronized時,編譯器會在生成的位元組碼中插入monitorenter和monitorexit指令,確保在方法執行前後正確地獲取和釋放對象的Monitor。

本文已收錄於我的個人博客:碼農Academy的博客,專註分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中間件、架構設計、面試題、程式員攻略等


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

-Advertisement-
Play Games
更多相關文章
  • 1.知識補充 1.1 nolocal關鍵字 在之前的課程中,我們學過global關鍵字。 name = 'root' def outer(): name = "武沛齊" def inner(): global name name = 123 inner() print(name) # 武沛齊 out ...
  • 對於實體中包含有公共欄位,像create_at,create_time,update_at,update_time來說,我們沒有必要在每個實體的crud操作中複製同樣的代碼,這樣代碼的味道很壞,我們應該使用mybatis的攔截器機制,將公共欄位統一處理;當然mybatis-puls在實現上更加優雅, ...
  • 拓展閱讀 常見免費開源繪圖工具 OmniGraffle 創建精確、美觀圖形的工具 UML-架構圖入門介紹 starUML UML 繪製工具 starUML 入門介紹 PlantUML 是繪製 uml 的一個開源項目 UML 等常見圖繪製工具 繪圖工具 draw.io / diagrams.net 免 ...
  • 1. 工程搭建 前端的工程運行流程: 進入項目目錄執行cmd命令: 若是第一次啟動需要依次輸入如下命令: npm install npm run build npm run dev 之後直接執行 npm run dev 即可! 1.1 新建maven工程 新建maven工程blog作為父工程,然後在 ...
  • 新網站對接到KC的部署 kc的環境 向kc申請自己的客戶端 kc的登錄介面 通過code換token介面 刷新token介面 kc的用戶信息介面 kc的jwt token說明 1. kc的環境 測試環境:https://test-kc.xxx.com 預發佈環境:https://pre-kc.xxx ...
  • PDF 文件是共用和分發文檔的常用選擇,但提取和再利用 PDF 文件中的內容可能會非常麻煩。而利用 Python 將 PDF 文件轉換為 HTML 是解決此問題的理想方案之一,這樣做可以增強文檔可訪問性,使文檔可搜索,同時增強文檔在不同場景中的實用性。此外,HTML 格式使得搜索引擎能夠對內容進行索 ...
  • 本文介紹基於Python語言,讀取Excel表格文件數據,並基於其中某一列數據的值,將這一數據處於指定範圍的那一行加以複製,並將所得結果保存為新的Excel表格文件的方法~ ...
  • 本文提供了一份全面的Kubernetes(K8S)命令指南,旨在幫助用戶掌握和運用K8S的各種命令。 關註【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩,復旦機器人智能實驗室成員,阿裡雲認證的資深架 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...