多線程系列(九) -ReentrantLock常用方法詳解

来源:https://www.cnblogs.com/dxflqm/p/18033936
-Advertisement-
Play Games

在上一篇文章中,我們介紹了ReentrantLock類的一些基本用法,今天我們重點來介紹一下ReentrantLock其它的常用方法,以便對ReentrantLock類的使用有更深入的理解。 ...


一、簡介

在上一篇文章中,我們介紹了ReentrantLock類的一些基本用法,今天我們重點來介紹一下ReentrantLock其它的常用方法,以便對ReentrantLock類的使用有更深入的理解。

二、常用方法介紹

2.1、構造方法

ReentrantLock類有兩個構造方法,核心源碼內容如下:

/**
 * 預設創建非公平鎖
 */
public ReentrantLock() {
    sync = new NonfairSync();
}
/**
 * fair為true表示是公平鎖,fair為false表示是非公平鎖
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

相比於synchronized同步鎖,ReentrantLock有一個很大的特點,就是開發人員可以手動指定採用公平鎖機制還是非公平鎖機制。

公平鎖:顧名思義,就是每個線程獲取鎖的順序是按照線程排隊的順序來分配的,最前面的線程總是最先獲取到鎖。

  • 優點:所有的線程都有機會得到資源
  • 缺點:公平鎖機制實現比較複雜,程式流程比較多,執行速度比較慢

非公平鎖:每個線程獲取鎖的順序是隨機的,並不會遵循先來先得的規則,任何線程在某時刻都有可能直接獲取並擁有鎖,之前介紹的synchronized其實就是一種非公平鎖

  • 優點:公平鎖機制實現相對比較簡單,程式流程比較少,執行速度比較快
  • 缺點:有可能某些線程一直拿不到鎖,導致餓死

ReentrantLock預設的構造方法是非公平鎖,如果想要構造公平鎖機制,只需要傳入true就可以了。

示例代碼如下:

public static void main(String[] args) {
    // 創建公平鎖實現機制
    Lock lock = new ReentrantLock(true);

    // 創建5個線程
    for (int i = 0; i < 5; i++) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 啟動了!");

                // 嘗試獲取鎖
                lock.lock();
                try {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 獲得鎖!");
                } finally {
                    lock.unlock();
                }
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 啟動了!
ThreadName:Thread-1, 啟動了!
ThreadName:Thread-0, 獲得鎖!
ThreadName:Thread-1, 獲得鎖!
ThreadName:Thread-2, 啟動了!
ThreadName:Thread-2, 獲得鎖!
ThreadName:Thread-3, 啟動了!
ThreadName:Thread-3, 獲得鎖!
ThreadName:Thread-4, 啟動了!
ThreadName:Thread-4, 獲得鎖!

從日誌上可以看到,啟動順序為0,1,2,3,4,獲取鎖的順序為0,1,2,3,4,啟動與獲取鎖的排隊機制一致。

假如我們構造方法裡面的把true改成false,也就是非公平鎖機制,在看看運行效果,結果如下:

ThreadName:Thread-1, 啟動了!
ThreadName:Thread-2, 啟動了!
ThreadName:Thread-1, 獲得鎖!
ThreadName:Thread-0, 啟動了!
ThreadName:Thread-2, 獲得鎖!
ThreadName:Thread-3, 啟動了!
ThreadName:Thread-3, 獲得鎖!
ThreadName:Thread-0, 獲得鎖!
ThreadName:Thread-4, 啟動了!
ThreadName:Thread-4, 獲得鎖!

從日誌上可以看到,啟動順序為1,2,0,3,4,獲取鎖的順序為1,2,3,0,4,線程啟用與獲取鎖的順序不一致。

從實際的運行結果看,非公平鎖要比公平鎖執行速度要快一些,當線程數越多的時候,效果越明顯。

2.2、核心方法

ReentrantLock類的核心方法就比較多了,如下表!

方法 描述
public void lock() 阻塞等待獲取鎖;不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted一樣會繼續嘗試
public void lockInterruptibly() 當前線程未被中斷,則獲取鎖;允許在等待時由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回
public boolean tryLock() 嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false
public boolean tryLock(long timeout, TimeUnit unit) 在一段時間內嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false
public void unlock() 釋放鎖
public Condition newCondition() 條件實例,用於線程等待/通知模式
public int getHoldCount() 獲取當前線程持有此鎖的次數
public boolean isHeldByCurrentThread() 檢測是否被當前線程持有
public boolean isLocked() 查詢此鎖是否由任意線程持有
public final boolean isFair() 如果是公平鎖返回true,否則返回false
public final boolean hasQueuedThreads() 查詢是否有線程正在等待
public final boolean hasQueuedThread(Thread thread) 查詢給定線程是否正在等待獲取此鎖
public final int getQueueLength() 獲取正等待獲取此鎖的線程數
public boolean hasWaiters(Condition condition) 是否存在正在等待並符合相關給定條件的線程
public int getWaitQueueLength(Condition condition) 正在等待並符合相關給定條件的線程數量

雖然方法很多,但是實際上常用方法就那麼幾個,下麵我們主要抽一些常用的方法進行介紹。

2.2.1、tryLock 方法

lock()lockInterruptibly()tryLock()tryLock(long timeout, TimeUnit unit)這幾個方法,目的其實是一樣的,都是為了獲取鎖,只是針對不同的場景做了單獨的處理。

lock():阻塞等待獲取鎖,如果沒有獲取到會一直阻塞,即使檢測到Thread.isInterrupted一樣會繼續嘗試;

  • lockInterruptibly():同樣也是阻塞等待獲取鎖,稍有不同的是,允許在等待時由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回
  • tryLock():表示嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false,不會阻塞等待獲取鎖
  • tryLock(long timeout, TimeUnit unit):表示在一段時間內嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false

其中tryLock(long timeout, TimeUnit unit)方法的應用最廣泛,因為它能防止程式發生死鎖,即使在一段時間內沒有獲取鎖,也會自動退出,不會一直阻塞。

我們可以看一個簡單的例子,如下!

public static void main(String[] args) {
    // 創建公平鎖實現機制
    Lock lock = new ReentrantLock();

    // 創建5個線程
    for (int i = 0; i < 5; i++) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                boolean flag = false;
                try {
                    // 嘗試3秒內獲取鎖
                    flag = lock.tryLock(3, TimeUnit.SECONDS);
                    if(flag){
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 獲取到鎖");
                        // 模擬進行5秒的業務操作
                        Thread.sleep(5000);
                    } else {
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (flag){
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 釋放對象");
                        lock.unlock();
                    }
                }
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 獲取到鎖
ThreadName:Thread-3, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-1, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-2, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-4, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-0, 釋放對象

可以很清晰的看到,非Thread-0線程嘗試了 3 秒沒有獲取到鎖,自動放棄;如果換成lock()方法進行獲取鎖,線程Thread-0如果不釋放鎖,其它線程會一直阻塞。

2.2.2、unlock 方法

unlock()方法也是常用方法,表示釋放鎖。當獲取到鎖之後,一定要手動釋放鎖,否則可能會造成其它程式執行出現問題,通常用在finally方法塊裡面。

// 阻塞等待獲取鎖
lock.lock();
try {
    // 業務操作...
} finally {
	// 一定要釋放鎖
    lock.unlock();
}
2.2.3、newCondition 方法

newCondition()方法,在上文中介紹過,ReentrantLockCondition結合,可以實現線程之間的等待/通知模型。

簡單的示例,如下!

public class Counter {

    private final Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    private int count;

    public void await(){
        // 加鎖
        lock.lock();
        try {
            // 讓當前線程進入等待狀態,並釋放鎖
            condition.await();
            System.out.println("await等待結束,count:" + getCount());
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }


    public void signal(){
        // 加鎖
        lock.lock();
        try {
            count++;
            // 喚醒某個等待線程
            condition.signal();
            System.out.println("signal 喚醒通知完畢");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 先啟動執行等待的線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.await();
            }
        }).start();

        Thread.sleep(3000);

        // 過3秒,再啟動執行通知的線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.signal();
            }
        }).start();
    }
}

運行一下程式,結果如下:

signal 喚醒通知完畢
await等待結束,count:1
2.2.4、getHoldCount 方法

getHoldCount()方法的作用是返回的是當前線程調用lock()的次數。

示例代碼如下:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();

    new Thread(new Runnable() {

        @Override
        public void run() {
            // 第一次獲取鎖
            lock.lock();
            try {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", getHoldCount:" +  lock.getHoldCount());

                // 第二次獲取鎖
                lock.lock();
                try {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", getHoldCount:" +  lock.getHoldCount());
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }
    }).start();
}

運行一下程式,結果如下:

ThreadName:Thread-0, getHoldCount:1
ThreadName:Thread-0, getHoldCount:2

側面也證明瞭一點,ReentrantLocksynchronized一樣,鎖都具有可重入特性,也就是說同一個線程多次調用同一個ReentrantLocklock()方法,可以再次進入方法體,無需阻塞等待。

2.2.5、isLocked 方法

isHeldByCurrentThread()isLocked()方法都是用於檢測鎖是否被持有。

其中isHeldByCurrentThread()方法表示此鎖是否由當前線程持有;isLocked()方法表示此鎖是否由任意線程持有。

我們看一個簡單的示例,如下:

public class Counter {

    private ReentrantLock lock = new ReentrantLock();

    public void methodA(){
        lock.lock();
        try {
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 當前線程是否持有鎖:" +  lock.isHeldByCurrentThread());
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 任意線程是否持有鎖:" +  lock.isLocked());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 當前線程是否持有鎖:" +  lock.isHeldByCurrentThread());
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 任意線程是否持有鎖:" +  lock.isLocked());
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.methodA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.methodB();
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 當前線程是否持有鎖:true
ThreadName:Thread-0, 任意線程是否持有鎖:true
ThreadName:Thread-1, 當前線程是否持有鎖:false
ThreadName:Thread-1, 任意線程是否持有鎖:true

從日誌結果很容易理解,Thread-0線程持有鎖,因此調用isHeldByCurrentThread()isLocked()方法,返回結果都是trueThread-1線程沒有持有鎖,因此isHeldByCurrentThread()方法返回falseisLocked()方法返回true

2.2.6、isFair 方法

isFair()方法用來獲取此鎖是否公平鎖。

簡單的示例,如下:

ReentrantLock lock = new ReentrantLock(true);
System.out.println("是否公平鎖:" +  lock.isFair());

輸出結果如下:

是否公平鎖:true

ReentrantLock預設的是非公平鎖,當通過構造方法顯式傳入true時,採用的是公平鎖機制

2.2.5、hasQueuedThreads 方法

hasQueuedThreads()hasQueuedThread()方法都用於查詢是否有線程等待獲取鎖,稍有不同的是:hasQueuedThreads()方法表示查詢是否有線程正在等待獲取鎖;hasQueuedThread()方法表示查詢給定線程是否正在等待獲取此鎖。

另外還有一個getQueueLength()方法,表示獲取正等待獲取此鎖的線程數。

我們看一個簡單的示例,如下:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();

    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadA.start();

    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadB.start();

    // 等待線程都啟動完畢
    Thread.sleep(1000);

    System.out.println("查詢是否有線程正在等待:" + lock.hasQueuedThreads());
    System.out.println("查詢處於等待的線程數:" + lock.getQueueLength());
    System.out.println("threadA 是否處於等待狀態:" +  lock.hasQueuedThread(threadA));
    System.out.println("threadB 是否處於等待狀態:" +  lock.hasQueuedThread(threadB));
}

輸出結果如下:

查詢是否有線程正在等待:true
查詢處於等待的線程數:1
threadA 是否處於等待狀態:false
threadB 是否處於等待狀態:true

從日誌上可以清晰的看到,線程threadA先獲取了鎖,線程threadB處於等待獲取鎖的狀態,處於等待的線程數為1

2.2.7、hasWaiters 方法

hasWaiters()getWaitQueueLength()方法,支持傳入condition條件對象進行查詢。

其中hasWaiters()方法表示查詢是否存在正在等待並符合相關給定條件的線程;getWaitQueueLength()方法表示查詢正在等待並符合相關給定條件的線程數量。

我們看一個簡單的示例,如下:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                condition.await();
                System.out.println("await等待結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadA.start();

    // 睡眠1秒
    Thread.sleep(1000);

    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("是否存在正在等待並符合相關給定條件的線程:" + lock.hasWaiters(condition));
                System.out.println("正在等待並符合相關給定條件的線程數量:" + lock.getWaitQueueLength(condition));
                Thread.sleep(5000);
                condition.signal();
                System.out.println("signal 喚醒通知完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadB.start();
}

輸出結果如下:

是否存在正在等待並符合相關給定條件的線程:true
正在等待並符合相關給定條件的線程數量:1
signal 喚醒通知完畢
await等待結束

需要註意的是,調用condition對象的方法,必須要在獲取鎖的方法體內執行。

三、小結

本文主要圍繞ReentrantLock類的核心方法進行了一些知識總結,其中最常用方法的主要就兩個,tryLock(long timeout, TimeUnit unit)unlock(),通過它可以實現線程同步安全的效果。

本文內容比較多,如果有不正之處,請多多諒解,並歡迎批評指出。

四、參考

1、https://www.cnblogs.com/xrq730/p/4855538.html


作者:程式員志哥
出處:pzblog.cn
資源:微信搜【程式員志哥】關註我,回覆 【技術資料】有我準備的一線程式必備電腦書籍、大廠面試資料和免費電子書。 希望可以幫助大家提升技術和能力。


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

-Advertisement-
Play Games
更多相關文章
  • 寫在前面 我知道自己現在的狀態很不好,以為放個假能好好放鬆下心情,結果昨晚做夢還在工作,調試代碼,和領導彙報工作。 天吶,明明是在放假,可大腦還在考慮工作的事,我的天那,這是怎麼了? Vue頁面參數傳遞 1、任務拆解 頁面跳轉時帶上當前電子書id參數ebookId 新增/編輯文檔時,讀取電子書id參 ...
  • 枚舉Enum是在多種語言中都有的一種數據類型,用於表示一組特定相關的常量數據集合,如性別(男、女)、數據狀態(可用、禁用)、垂直對齊(頂端、居中、底部)、星期等。特點是數據值固定,不會變,存儲和顯示的內容不同。然而在JavaScript中並沒有枚舉Enum類型,TypeScript算是有(本文中暫沒... ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、是什麼 許可權是對特定資源的訪問許可,所謂許可權控制,也就是確保用戶只能訪問到被分配的資源 而前端許可權歸根結底是請求的發起權,請求的發起可能有下麵兩種形式觸發 頁面載入觸發 頁面上的按鈕點擊觸發 總的來說,所有的請求發起都觸發自前端路由或 ...
  • 即使再小再簡單的需求,作為研發開發完畢之後,我們可以直接上線麽?其實很多時候事故往往就是由於“不以為意”發生的。事故的發生往往也遵循“墨菲定律”,這就要求我們更要敬畏線上,再小的需求點都需要經過嚴格的測試驗證才能上線。 ...
  • 指針和引用 當我們需要在程式中傳遞變數的地址時,可以使用指針或引用。它們都可以用來間接訪問變數,但它們之間有一些重要的區別。 指針是一個變數,它存儲另一個變數的地址。通過指針,我們可以訪問存儲在該地址中的變數。指針可以被重新分配,可以指向不同的變數,也可以為NULL。指針使用*運算符來訪問存儲在地址 ...
  • 與TXT文本文件,PDF文件更加專業也更適合傳輸,常用於正式報告、簡歷、合同等場合。項目中如果有使用Java將TXT文本文件轉為PDF文件的需求,可以查看本文中介紹的免費實現方法。 免費Java PDF庫 本文介紹的方法需要用到Free Spire.PDF for Java,該免費庫支持多種操作、轉 ...
  • 雲採用框架(Cloud Adoption Framework,簡稱CAF)為企業上雲提供策略和技術的指導原則和最佳實踐,幫助企業上好雲、用好雲、管好雲,併成功實現業務目標。本雲採用框架是基於服務大量企業客戶的經驗總結,將企業雲採用分為四個階段,並詳細探討企業應在每個階段採取的業務和技術策略;同時,還 ...
  • Excelize 是 Go 語言編寫的用於操作電子錶格辦公文檔的開源基礎庫,2024年2月26日,社區正式發佈了 2.8.1 版本,該版本包含了多項新增功能、錯誤修複和相容性提升優化。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...