1. 是什麼?
LockSupport是JUC包下的一個類,是用來創建鎖和其他同步類的基本線程阻塞原語。
相信大多數人看了這句話也還是不太明白它到底是啥東西,那你還記得等待喚醒機制嗎?之前實現等待喚醒機制可以用wait/notify,可以用await/signal,這個LockSupport就是它們的改良版。
2. 等待喚醒機制:
先來回顧一下等待喚醒機制。
先看看用wait/notify實現:
- wait/notify:
private static Object lockObj = new Object();
private static void waitNotify() {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockObj){
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
try { lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("線程" + Thread.currentThread().getName() + "被喚醒");
}
}, "A").start();
new Thread(() -> {
synchronized (lockObj){
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
lockObj.notify();
System.out.println("線程" + Thread.currentThread().getName() + "喚醒另一個線程");
}
}, "B").start();
}
這段代碼就很簡單了,線程A先wait,線程B去notify,線程B執行完了A就被喚醒了,這就是最開始學的等待喚醒機制。
假如我現在註釋掉synchronized,如下:
new Thread(() -> {
//synchronized (lockObj){
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
try { lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("線程" + Thread.currentThread().getName() + "被喚醒");
//}
}, "A").start();
再次運行,結果報錯了,如下:
得出第一個結論:wait/notify必須中同步代碼塊中才能使用。
如果先執行notify,再執行wait,情況如何呢?讓A線程睡3秒鐘,使B先執行,先notify,然後A中wait,執行結果如下:
可以發現”線程A被喚醒“這句話一直沒有打印出來。
得出第二個結論:先notify再wait的話,程序無法被喚醒。
再來看看用await/notify實現:
- await/signal:
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static void awaitSignal(){
new Thread(() -> {
lock.lock();
try {
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
condition.await();
System.out.println("線程" + Thread.currentThread().getName() + "被喚醒");
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
try {
lock.lock();
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
condition.signal();
System.out.println("線程" + Thread.currentThread().getName() + "喚醒另一個線程");
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
}
這個是用await/signal實現的等待喚醒機制。
假如註釋掉lock和unlock這兩個操作,再次執行,還是會拋之前那個異常,即IllegalMonitorStateException。
得出第一個結論:await/notify必須伴隨lock/unlock出現。
假如先signal,再await,情況也是和之前用wait/notify一樣,await的線程一直沒被喚醒。
得出第二個結論:必須先await再signal。
所以不管是wait/notify還是await/signal,都有兩個限制條件:
線程要先獲得並持有鎖,必須中鎖塊中執行;
必須先等待後喚醒。
3. LockSupport怎麼用?
LockSupport主要就是用park(等待)和unpark(喚醒)方法來實現等待喚醒。它的原理就是使用了一種名爲permit(許可證)的概念來實現等待喚醒功能,每個線程都有一個許可證,許可證只有兩個值,一個是0,一個是1。默認值是0,表示沒有許可證,就會被阻塞。那誰來發放許可證呢,就是unpark方法。這兩個方法底層其實是UNSAFE類的park和unpark方法,調用park時,將permit的值設置爲0,調用unpark時,將permit的值設置爲1。
用法如下:
private static void lockSupportTest(){
Thread a = new Thread(() -> {
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
LockSupport.park(); // 等待
System.out.println("線程" + Thread.currentThread().getName() + "被喚醒");
}, "A");
a.start();
Thread b = new Thread(() -> {
System.out.println("線程" + Thread.currentThread().getName() + "進來了");
LockSupport.unpark(a); // 喚醒
}, "B");
b.start();
}
首先它不需要再同步塊中使用,這是第一個優點。那麼先unpark再park會不會報錯呢?要知道另兩種方式先喚醒再等待的話,都會導致線程無法被喚醒的。假如我先unpark,再park,其實也是可以的,相當於提前發放了通行證,先給A線程unpark了,那麼A線程執行的時候,就相當於沒有park這一行。
LockSupport總結:是一個線程阻塞喚醒的工具類,所有方法都是靜態方法,可以讓線程在任意位置阻塞,其底層調用的是UNSAFE類的native方法。每調用一次unpark方法,permit就會變成1,每調一次park方法,就會消耗掉一個許可證,即permit就變成0,每個線程都有一個permit,permit最多也就一個,多次調用unpark也不會累加。因爲這是根據是否有permit去判斷是否要阻塞線程的,所以,先unpark再park也可以,跟順序無關,只看是否有permit。如果先unpark了兩次,再park兩次,那麼線程還是會被阻塞,因爲permit不會累加,unpark兩次,permit的值還是1,第一次park的時變成0了,所以第二次park就會阻塞線程。