多線程系列(九) -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
  • 隨著Aspire發佈preview5的發佈,Microsoft.Extensions.ServiceDiscovery隨之更新, 服務註冊發現這個屬於老掉牙的話題解決什麼問題就不贅述了,這裡主要講講Microsoft.Extensions.ServiceDiscovery(preview5)以及如何 ...
  • 概述:通過使用`SemaphoreSlim`,可以簡單而有效地限制非同步HTTP請求的併發量,確保在任何給定時間內不超過20個網頁同時下載。`ParallelOptions`不適用於非同步操作,但可考慮使用`Parallel.ForEach`,儘管在非同步場景中謹慎使用。 對於併發非同步 I/O 操作的數量 ...
  • 1.Linux上安裝Docken 伺服器系統版本以及內核版本:cat /etc/redhat-release 查看伺服器內核版本:uname -r 安裝依賴包:yum install -y yum-utils device-mapper-persistent-data lvm2 設置阿裡雲鏡像源:y ...
  • 概述:WPF界面綁定和渲染大量數據可能導致性能問題。通過啟用UI虛擬化、非同步載入和數據分頁,可以有效提高界面響應性能。以下是簡單示例演示這些優化方法。 在WPF中,當你嘗試綁定和渲染大量的數據項時,性能問題可能出現。以下是一些可能導致性能慢的原因以及優化方法: UI 虛擬化: WPF提供了虛擬化技術 ...
  • 引言 上一章節介紹了 TDD 的三大法則,今天我們講一下在單元測試中模擬對象的使用。 Fake Fake - Fake 是一個通用術語,可用於描述 stub或 mock 對象。 它是 stub 還是 mock 取決於使用它的上下文。 也就是說,Fake 可以是 stub 或 mock Mock - ...
  • 為.net6在CentOS7上面做準備,先在vmware虛擬機安裝CentOS 7.9 新建CentOS764位的系統 因為CentOS8不更新了,所以安裝7;簡單就一筆帶過了 選擇下載好的操作系統的iso文件,下載地址https://mirrors.aliyun.com/centos/7.9.20 ...
  • 經過前面幾篇的學習,我們瞭解到指令的大概分類,如:參數載入指令,該載入指令以 Ld 開頭,將參數載入到棧中,以便於後續執行操作命令。參數存儲指令,其指令以 St 開頭,將棧中的數據,存儲到指定的變數中,以方便後續使用。創建實例指令,其指令以 New 開頭,用於在運行時動態生成並初始化對象。方法調用指... ...
  • LiteDB 是一個輕量級的嵌入式 NoSQL 資料庫,其設計理念與 MongoDB 類似,但它是完全使用 C# 開發的,因此與 C# 應用程式的集成非常順暢。與 SQLite 相比,LiteDB 提供了 NoSQL(即鍵值對)的數據存儲方式,並且是一個開源且免費的項目。它適用於桌面、移動以及 We ...
  • 1 開源解析和拆分文檔 第三方的工具去對文件解析拆分,去將我們的文件內容給提取出來,並將我們的文檔內容去拆分成一個小的chunk。常見的PDF word mark down, JSON、HTML。都可以有很好的一些模塊去把這些文件去進行一個東西去提取。 優勢 支持豐富的文檔類型 每種文檔多樣化選擇 ...
  • OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。 要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪 ...