同步控制和鎖,ReenterLock和Condition的詳細使用

hidecode發表於2020-12-29

多執行緒團隊合作:同步控制和鎖

同步控制是併發程式必不可少的重要手段。synchronized關鍵字就是一種最簡單的控制方法。同時,wait()和notify()方法起到了執行緒等待和通知的作用。這些工具對於實現複雜的多執行緒協作起到了重要的作用。接下來將介紹synchronized,wait,notify方法的代替品(或者說是增強版)——重入鎖,這個專題需要大家對多執行緒基本的內部鎖synchronized,wait, notify方法先有基本的認識。

1 synchronized的功能擴充套件: 重入鎖

重入鎖完全可以代替synchronized關鍵字。在早期JDK版本,重入鎖的效能遠遠優於synchronized關鍵字,在JDK後期版本,對synchronized關鍵字做了大量的優化,使得兩者的效能差不多。

下面展示一段簡單的重入鎖ReentrantLock使用案例:

public class ReenterLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i=0;

    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
//            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
//                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock r1 = new ReenterLock();
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

​ 上述程式碼建立了一個全域性的ReentrantLock物件,這個物件就是重入鎖物件,該物件的lock()和unlock()方法之間7~12行的程式碼區域就是重入鎖的保護臨界區,確保了多執行緒對i變數的操作安全性。

​ 從這段程式碼可以看到,與synchronized相比,重入鎖有著顯示操作的過程。開發人員必須手動指定何時加鎖 ,何時釋放鎖。也正是因為這樣,重入鎖邏輯控制遠遠要好於synchronized。但值得注意的是,在退出臨界區時,必須記得要釋放鎖,否者永遠沒有機會再訪問臨界區了,會造成其執行緒的飢餓甚至是死鎖。

​ 重入鎖之所以被稱作重入鎖是因為重入鎖是可以反覆進入的。當然,這裡的反覆進入僅僅侷限於一個執行緒。上訴程式碼還可以這樣寫:

    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
                lock.unlock();
            }
        }
    }

​ 在這種情況下,一個執行緒連續兩次獲得同一把鎖。這是允許的!但要注意的是,如果一個執行緒多次獲得鎖,那麼在釋放鎖的時候,也必須釋放相同次數。如果釋放的次數多了,那麼會得到一個java.lang.IllegalMonitorStateException異常,反之,如果釋放所得次數少了,MAME相當於縣城還持有這個鎖,因此,其他執行緒也無法進入臨界區

​ 處使用上的靈活性以外,重入所還提供了一些高階功能。比如重入鎖提供的中斷處理的能力

1.1 中斷響應

​ 重入鎖除了提供上述的基本功能外,還提供了一些高階功能。比如,重入鎖可以提供中斷處理的能力。這是一個非常重要的功能,synchronized是沒有中斷功能的。在等待鎖的過程中,程式可以根據需要取消對鎖的請求。這是synchronized辦不到的。也就是說,重入鎖具有解除死鎖的功能。

​ 比如你和朋友越好一起去打球,如果你等了半個小時朋友沒有到,你突然接到一個電話,說由於突發情況,朋友不能如期錢來了,那麼你一定掃興的達到回府了。中斷正是提供了一套類似的機制。如果一個縣城正在等待鎖,那麼他依然可以收到一個通知,被告知無需等待,可以停止工作了,這種情況對於處理死鎖是有一定幫助的。

​ 下面的程式碼產生了一個死鎖,得益於鎖的中斷,我們可以輕易的解決這個死鎖:

public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public IntLock(int lock) {
        this.lock = lock;
    }

    public void run() {
        try {
            if (this.lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread())
                lock1.unlock();
            if (lock2.isHeldByCurrentThread())
                lock2.unlock();
            System.out.println(this.lock + "執行緒退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t2.interrupt();
    }
}

​ 執行緒t1和執行緒t2啟動後,t1先佔用lock1,再佔用lock2;t2先佔用lock2,再請求lock1。這樣很容易形成t1和t2之間的互相等待,造成死鎖。在這裡,對鎖的請求,統一使用lockInterruptibly()方法。這是一個可以對中斷進行響應的鎖申請動作,即在等待鎖的過程中可以響應中斷。

​ 在t1和t2執行緒start後,主執行緒39行main進入休眠,此時t1和t2執行緒處於死鎖狀態,然後主執行緒第40行main中斷t2執行緒,故t2會放棄對lock1的請求,同時釋放lock2。這個操作使得t1可以獲得lock2從而繼續執行下去。

​ 執行上訴程式碼,將輸出:

2執行緒退出
1執行緒退出
java.lang.InterruptedException
	at com.lxs.demo.IntLock.run(IntLock.java:24)
	at java.lang.Thread.run(Thread.java:745)

​ 可以看到,中斷後,兩個執行緒雙雙退出。但真正完成工作的只有t1。而t2放棄任務直接退出,釋放資源。

1.2 鎖申請等待限時

​ 除了等待外部通知之外,還有一種避免死鎖的方法,就是限時等待。通常,我們不會預料到系統在什麼時候會產生死鎖,就無法主動的解除死鎖,最好的系統設計方式是,這個系統根本就不會產生死鎖。我們可以用tryLock()方法進行限時等待。

​ 下面這段程式碼展示了限時等待鎖的使用:

public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();

    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("Get lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }

    public static void main(String [] args){
        TimeLock lock1 = new TimeLock();
        Thread t1 = new Thread(lock1);
        Thread t2 = new Thread(lock1);
        t1.start();
        t2.start();
    }
}

輸出結果:

Get lock failed

​ 在這裡,tryLock()接收兩個引數,一個表示等待時長,另一個表示計時單位。這裡設定為秒,時長為5,表示執行緒在這個鎖的請求中,最多等待5秒。如果超過5秒還沒有得到鎖就返回false。如果成功就返回true。
​ 在本例中,由於佔用鎖的執行緒會持有鎖長達6秒,故另外一個執行緒無法在5秒內獲得鎖,因此,對鎖的請求會失敗。

​ tryLock()方法也可以不帶引數直接執行,在這種情況下,當前程式會嘗試獲得鎖,如果鎖並未被其他程式佔用,則申請就會成功,立即返回true。如果鎖被其他執行緒佔用,會立即返回false。這種模式不會引起執行緒的等待,因此不會造成死鎖。下面演示了這種使用方式:

public class TryLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public TryLock(int lock) {
        this.lock = lock;
    }

    public void run() {
        if (lock == 1) {
            while (true) {
                if(lock1.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    }finally {
                        lock1.unlock();
                    }
                }
            }
        } else {
            while (true) {
                if(lock2.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally {
                                lock1.unlock();
                            }
                        }
                    }finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String [] args){
        TryLock lock1 = new TryLock(1);
        TryLock lock2 = new TryLock(2);
        Thread t1 = new Thread(lock1);
        Thread t2 = new Thread(lock2);
        t1.start();
        t2.start();
    }
}

​ 上述程式碼中,採用了非常容易死鎖的加鎖順序。也就是先讓執行緒t1請求lock1,在請求lock2,而讓t2先請求lock2,在請求lock1。在一般情況下,這樣會導致t1,t2互相等待,從而引起死鎖。
​ 但是採用tryLock後,這種情況得到了改善。由於執行緒不會傻傻的等待,而是不停的嘗試,因此,只要執行足夠長的時間,執行緒總是會獲得所需要的資源,從而正常執行(這裡以執行緒能同時獲得lock1和lock2兩把鎖視為正常執行。

程式碼執行結果如下:

12 my job done
11 my job done

1.3 公平鎖

​ 在大多數情況下,鎖的申請都是非公平的。也就是說,執行緒1首先請求了鎖A,接著執行緒2也請求了鎖A。那麼鎖A可用時,執行緒1可以獲得鎖還是執行緒2可以獲得鎖呢?這是不一定的,系統只會從這個鎖的等待佇列中隨機挑取一個。因此不能保證公平性。
​ 而接下來要講的公平鎖,他會按照時間的先後順序,保證先到者先得,後到者後得。所以,公平鎖的最大特點就是,他不會產生飢餓現象。
​ 注意:如果執行緒採用synchronized進行互斥,那麼產生的鎖是非公平的。而重入鎖允許我們進行公平性設定。他有一個如下的建構函式:

public ReentranLock(boolean fair);

​ 當引數fair為true時,表示鎖是公平的。公平鎖看起來很優美,但是要實現公平鎖,必然要求系統維護一個有序佇列,因此對公平鎖得到實現成本比較高,意味著公平鎖的效率非常低下,因此,在預設情況下,鎖是非公平的。如果沒有什麼特別的需求,儘量別用公平鎖。

​ 下面程式碼能很好的凸顯公平鎖的特點:

public class FairLock implements Runnable {
    public static ReentrantLock fairLock = new ReentrantLock(true);
//    public static ReentrantLock fairLock = new ReentrantLock();

    public void run() {
        while (true){
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName());
            }finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String [] args){
        FairLock r1 = new FairLock();
        Thread t1 = new Thread(r1,"Thread_t1");
        Thread t2 = new Thread(r1,"Thread_t2");
        t1.start();
        t2.start();
    }
}

程式碼執行結果:

Thread_t1
Thread_t2
Thread_t1
Thread_t2
Thread_t1
Thread_t2

可以看到,執行緒的排程是公平的。

對上面的ReentantLock的幾個方法整理如下

  • lock():獲得鎖,如果鎖已經被佔用,則等待。
  • lockInterruptibly():獲得鎖,但優先響應中斷。
  • tryLock():嘗試獲得鎖,如果成功,則返回true,失敗返回false。該方法不等待,立即返回
  • tryLock(long time, TimeUnit unit):在給定時間內嘗試獲得鎖。
  • unlock():釋放鎖。

2 重入鎖的好搭檔:Condition

​ 如果大家瞭解object.wait()方法和object.notify()方法的,那麼就能很容易理解condition物件了。他和wait()和notify()方法的作用是基本相同的。但是wait()和notify()方法是與synchronized關鍵字組合使用的,而condition是與重入鎖相關聯的。
Condition介面提供的基本方法如下:

void await() throws InterrupteException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterrupteException;
boolean await(long time, TimeUnit unit) throws InterrupteException;
boolean awaitUntil(Data deadline) throws InterrupteException;
void signal();
void signalAll();

以上方法含義如下:

  • await()方法會使當前執行緒等待,同時釋放當前鎖,當其他執行緒使用signal()或signalAll()方法時,執行緒會重新獲得鎖並繼續執行。或者當執行緒被中斷時,也能跳出等待。這和object.wait()方法很相似。

  • awaitUninterruptibly()方法與wait()方法相同,唯一的不同點是,該方法不會再等待的過程中響應中斷。

  • signal()方法用於喚醒一個在等待中的執行緒。signalAll()會喚醒所有正在等待的執行緒。這和object.notify()方法很相似。

下面程式碼簡單的演示了Condition的作用:

public class ReenterLockCondition implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public void run() {
        try{
            lock.lock();
            condition.await();
            System.out.println("Thread is going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String [] args) throws InterruptedException {
        ReenterLockCondition r1 = new ReenterLockCondition();
        Thread t1= new Thread(r1);
        t1.start();
        Thread.sleep(2000);
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

​ 第3行程式碼先通過lock生成一個與之繫結的condition物件。第8行程式碼要求執行緒在condition物件上進行等待。主執行緒main在兩秒後發出signal通知,告知等待在condition上的執行緒可以繼續執行了。

​ 和object.wait()和object.notify()一樣,當執行緒使用Condition.wait()時,要求執行緒持有相關的重入鎖,在condition.wait()呼叫後,這個執行緒會主動釋放這把鎖。並且,在condition.signal()方法呼叫時,也要求執行緒獲取相關的鎖。注意,在signal()方法呼叫之後,一定要釋放相關的鎖第24行,把鎖讓給其他執行緒。如果省略了24行,那麼,雖然已經喚醒了縣城t1,但是由於無法重新獲得鎖,因為也就無法真正的繼續執行。

3 允許多個執行緒同時訪問:訊號量(Semaphore)

​ 訊號量為多執行緒協作提供了更為強大的控制方法。廣義上說,訊號量是對鎖的擴充套件。無論是內部鎖synchronized還是重入鎖ReentranLock,一次都只允許一個執行緒訪問一個資源,而訊號量卻可以指定多個執行緒,同時訪問一個資源。訊號量主要提供了一下的建構函式:

public Semaphore(int permits);

public Semaphore(int permits, boolean fair); //第二個引數可以指定是否公平

​ 在構造訊號量時,必須指定訊號量的准入數,即同時能申請幾個許可。當每個執行緒只申請一個許可時,這就相當於指定了同時能有多少個執行緒可以訪問某個資源。訊號量的主要邏輯方法有:

public void acquire();

public void acquireUninterruptibly();

public boolean tryAcquire();

public boolean tryAcquire(long timeout, TimeUnit unit);

public void release();

​ acquire()方法嘗試獲得一個准入的許可。若無法獲得,則執行緒會等待,直到申請到許可或者當前執行緒被中斷。acquireUninterruptibly()方法與acquire()方法類似,但不響應中斷。tryAcquire()嘗試獲得一個許可,成功返回true失敗返回false,它不會進行阻塞等待,立即返回。release()用於線上程訪問資源結束後,釋放一個許可,以使其他等待許可的執行緒可以進行資源訪問。

下面是Semaphore的簡單使用:

public class SemapDemo implements Runnable {
    //5個一組輸出
    final Semaphore semp = new Semaphore(5);

    public void run() {
        try {
            semp.acquire();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + " done!");
            semp.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String []args){
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemapDemo demo = new SemapDemo();
        for(int i=0;i<20;i++){
            exec.submit(demo);
        }
    }
}

在本例中同時開啟了20個執行緒。觀察上述程式的輸出,你會發現執行緒以5個執行緒為一組依次輸出。

4 ReadWriteLock 讀寫鎖

​ ReadWriteLock 是JDK5中提供的讀寫分離鎖。讀寫鎖能有效的幫助減少鎖競爭,以提升系統效能。用鎖分離的機制來提升效能非常容易理解,比如縣城A1、A2、A3進行寫操作,執行緒B1、B2、B3進行讀操作,如果使用重入鎖或者內部鎖,從理論上說所有讀之間、讀和寫之間、寫和寫之間都是序列操作。當B1進行讀時,B2、B3則需要等待鎖。由於讀操作並不對資料的完整性造成破壞,這種等待顯然是不合理的。因此讀寫所就用了發揮的餘地。

​ 在這種情況下,讀寫所容許多個執行緒同時讀,是的B1、B2、B3之間並行。但是,考慮到資料完整性,寫寫,和讀寫操作依然是需要相互等待和持有鎖的。總的來說讀寫鎖約束訪問情況如下表

非阻塞阻塞
阻塞阻塞
  • 讀-讀不互斥:讀讀之間不阻塞。

  • 讀-寫互斥:讀阻塞寫,寫也會阻塞讀。

  • 寫-寫互斥:寫寫阻塞。

​ 如果系統中,讀操作次數遠遠大於寫操作,則讀寫鎖可以發揮最大的功效,提升系統效能。這裡給出一個稍微誇張的案例,來說明讀寫鎖對效能的幫助。

public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read success");
            return value;
        } finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock, int index) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = index;
            System.out.println("write success");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            public void run() {
                try {
                    demo.handleRead(readLock);
//                    demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {

            public void run() {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
//                    demo.handleWrite(lock, new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 18; i++) {
            new Thread(readRunnable).start();
        }

        for (int i = 18; i < 20; i++) {
            new Thread(writeRunnable).start();
        }
    }
}

​ 上述程式碼中,比較了使用讀寫鎖和普通鎖時,系統完成讀寫任務所需要的時間,這裡設定讀任務要比寫任務多得多

​ 從執行結果,可以看到,不用讀寫鎖,程式花費了20秒的時間才完成讀寫任務。採用讀寫鎖,程式需要3秒就完成讀寫任務了。

5 倒計數器:CoundownLatch

​ CountDownlatch是一個非常實用的多執行緒控制工具類,這裡簡單稱之為倒數計數器,這個工具通常用來控制執行緒等待,他可以讓某一個執行緒等待直到計數結束,在開始執行

​ 一種典型的場景就是火箭發射,在火箭發射前,為了保證萬無一失,往往做多想檢查,引擎才能點火執行,這個場景非常適合CountDownLatch,他可以是點火執行緒在等待所有檢查執行緒全部完工後在執行.

​ CountDownLatch的建構函式接收一個整數作為引數,即當前這個計數器的計數個數。

    public CountDownLatch(int count)

​ 下面示例演示CountDownLatch使用的方法

public class CountDownLatchDemo implements Runnable {
    static final CountDownLatch end = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();


    public void run() {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete!");
            end.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            exec.submit(demo);
        }
        end.await();
        System.out.println("Fire!");
        exec.shutdown();
    }
}

​ 上述程式碼第2行生成一個CountDownLatch例項,計數數量為10,這表示需要10個執行緒完成任務後等待在CountDownLatch上的執行緒才能繼續執行,程式碼第10行使用了CountDownLatch.countDown()方法,也就是通知CountDownLatch,一個執行緒已經完成了任務,到計數器減1。第21行使用CountDownLatch.await()方法,要求主執行緒等待所有檢查任務全部完成,待10個任務全部完成後,主執行緒才能繼續執行。

上述案例執行邏輯如下圖簡單表示

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CrIvOGrD-1609172826418)(assets/CountDownLatch%E6%89%A7%E8%A1%8C%E9%80%BB%E8%BE%91.png)]

主執行緒在CountDownLatch上等待,當所有的檢查執行緒任務全部完成後,主執行緒方能繼續執行

6 執行緒阻塞工具類:LockSupport

​ 首先看下suspend()方法卡死執行緒的例子

public class BadSuspend {
    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread{
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u){
                System.out.println("in "+ getName());
                Thread.currentThread().suspend();
            }
        }

    }

    // 導致resume不生效的執行順序可能是這樣的:
    // 列印t1 => t1在suspend => t2等待u釋放 => t1被resume => t2被resume => u釋放列印t2 => t2被suspend => 永遠無法結束
    public static void main(String []args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

上述案例執行示意圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-lDF7S3HD-1609172826422)(assets\suspend死鎖.png)]

​ 主函式呼叫了resume()方法,但是由於時間先後順序的緣,那個resume並沒有生效!這就導致了執行緒t2永遠被掛起,並且永遠佔用了物件u的鎖,這對於系統來來說可能是致命的。

​ LockSupport是一個非常方便的使用的執行緒阻塞工具,他可以線上程內任意位置讓執行緒阻塞。與Thread.suspend()方法相比,她彌補了由於resume方法發生導致執行緒無法繼續執行的情況。和Object.wait()方法相比,他不需要獲得某個物件的鎖,也不會丟擲InterruptedExeption異常。

​ 現在使用LockSupport重寫這個程式

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        LockSupport.unpark(t1);
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

​ 這個案例可以正常結束,不會因為pack()方法導致執行緒永久掛起,這是因為LockSupport類使用類似訊號量的機制,他為為每一個執行緒準備一個了一個許可,如果許可可用那麼pack()方法立即返回,並且消費這個許可(也就是把許可變為不可用),如果許可不可用,就會阻塞,而unpack方法則使得一個許可可用,這個特點使得即使unpack發生在pack方法之前,他也可以是下一次的pack()方法立即執行返回。

​ LockSupport.pack()方法還能支援中斷影響,但是和其他接受中斷函式不一樣,LockSupport.pack()方法不會丟擲InterruptedException異常。他只會默默返回,但是我們可以從Thread.interrupted()等方法中獲得中斷標記

public class LockSupportIntDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.interrupted()) {
                    System.out.println(getName() + "被中斷了");
                }
            }
            System.out.println(getName() + "執行結束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.interrupt();
        LockSupport.unpark(t2);
    }
}

class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}

    public void run() {
        synchronized (u) {
            System.out.println("in " + getName());
            LockSupport.park();
            if (Thread.interrupted()) {
                System.out.println(getName() + "被中斷了");
            }
        }
        System.out.println(getName() + "執行結束");
    }
}

public static void main(String[] args) throws InterruptedException {
    t1.start();
    Thread.sleep(100);
    t2.start();
    t1.interrupt();
    LockSupport.unpark(t2);
}

}


​	

​	上述程式碼29行中斷了出於pack()方法狀態的t1,之後,t1可以馬上響應這個中斷,並且返回,t1返回後外面等待的t2才可以進入臨界區,並最終由LockSupport.unpack(t2)操作使其執行結束。

相關文章