回顧
首先我們回顧一下前面四節(jié)所講的東西
1.線程的基本概念
2.synchronized,底層實(shí)現(xiàn)原理,鎖升級(無鎖-偏向鎖-輕量級鎖-重量級鎖)
3.volatile,線程隔離可見性,禁止指令重排序
4.AtomicXXX
5.各種UC同步框架(ReentrantLock,CountDownLatch,CyclicBarrier,Phaser,ReadWriteLock,Semaphore,Exchange)
6.synchronized 和 各種線程同步框架的ReenrantLock有何不同?
6.1.synchronizec:系統(tǒng)自帶,系統(tǒng)自動加鎖,自動解鎖,不可以出現(xiàn)多個(gè)不同的等待隊(duì)列,默認(rèn)進(jìn)行四種鎖的升級
6.2.ReentrantLock:需要自動加鎖,手動解鎖,可以出現(xiàn)多個(gè)不同的等待隊(duì)列,CAS的實(shí)現(xiàn)
本章我們補(bǔ)一個(gè)LockSupport,然后分析兩個(gè)面試題,緊接著我會教大家閱讀源碼的技巧,最后拿AQS作源碼分析
2.LockSupport
我們下面以幾個(gè)小程序案例,對LockSupport進(jìn)行詳細(xì)解釋,在以前我們要阻塞和喚醒某一個(gè)具體的線程有很多的限制,比如
因?yàn)閣ait方法需要釋放鎖,所以必須在synchronized中使用,否則會拋出異常lllegalMonitorStateException
nofity方法必須在synchronized中使用,并且應(yīng)該指定對象
synchronized,nofity,wait的對象必須一致,一個(gè)synchronized代碼塊中只能有一個(gè)線程用wait和nofity
以上諸多的使用不便,帶來了lockSupport的好處
先來看第一個(gè)小程序
package com.learn.thread.four;
import com.sun.tools.internal.ws.wsdl.document.soap.SOAPUse;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
public class TestLockSupport {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
// 使用LockSupport進(jìn)行線程堵塞
if (i == 5) {
LockSupport.park();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
});
thread.start();
}
}
上述代碼看起來是比較靈活的,可以手動控制是否上鎖,通過LockSupprt.park加鎖,有了加鎖機(jī)制,LockSupport也提供了令牌解鎖機(jī)制unpark
package com.learn.thread.four;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
public class TestLockSupport2 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
// 使用LockSupport進(jìn)行線程堵塞
if (i == 5) {
LockSupport.park();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
});
thread.start();
// 喚醒線程t,注意這里是比park先執(zhí)行的,當(dāng)前與釋放了一個(gè)令牌,當(dāng)park拿到這個(gè)令牌后不阻塞直接放行
LockSupport.unpark(thread);
}
}
注意,這里是一個(gè)park對應(yīng)著一個(gè)unpark
package com.learn.thread.four;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class TestLockSupport3 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
// 使用LockSupport進(jìn)行線程堵塞
if (i == 5) {
LockSupport.park();
}
// 使用LockSupport進(jìn)行線程堵塞(前面只會釋放一次鎖,這里會再次阻塞)
if (i == 8) {
LockSupport.park();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
});
thread.start();
LockSupport.unpark(thread);
}
}
2.1.由以上三個(gè)小程序,我們可以總結(jié)出以下幾點(diǎn)
LockSupport不需要synchronized就可以實(shí)現(xiàn)線程的阻塞和喚醒
LockSupport.unpark可以線程LockSupport.park,并且線程不會阻塞
如果一個(gè)線程處于等待狀態(tài),連續(xù)調(diào)用了兩次park方法,就會使該線程永遠(yuǎn)無法喚醒
LockSupport中的park 和unpark的實(shí)現(xiàn)原理
其實(shí)park()和unpark()的實(shí)現(xiàn)也是由Unsefa類提供的,而Unsefa類是由C和c++語言完成的,其實(shí)原理也是可以理解,它主要是通過一個(gè)變量作為標(biāo)志,變量在0-1之間來回切換,當(dāng)這個(gè)變量大于0的時(shí)候線程就獲得了“令牌”(unpark的操作),相反park的操作就是將這個(gè)變量變成0,用于識別令牌。
2.2.淘寶面試題1
實(shí)現(xiàn)一個(gè)容器,提供兩個(gè)方法add和size 寫兩個(gè)線程
線程1:添加十個(gè)元素到容器中
線程2:實(shí)時(shí)監(jiān)控元素個(gè)數(shù),當(dāng)個(gè)數(shù)到5個(gè)時(shí),線程2給出提示并結(jié)束
小程序1
我們先來分析一下小程序1的執(zhí)行流程:
通過內(nèi)部的List來new 一個(gè)ArraryList,在自定義的add方法直接調(diào)用list的add方法,在自定義的size方法調(diào)用list的size方法。然后小程序初始化這個(gè)List,啟動線程t1來做一個(gè)循環(huán),每次循環(huán)都添加一個(gè)對象,加一個(gè)打印,然后間隔一秒,在t2線程寫一個(gè)while循環(huán),實(shí)時(shí)監(jiān)控集合中對象的變換,如果數(shù)量達(dá)到5就結(jié)束t2的線程。
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
public class TestListAdd1 {
List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) {
TestListAdd1 testListAdd1 = new TestListAdd1();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
testListAdd1.add(new Object());
System.out.println(i);
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
// t2 線程監(jiān)聽集合的數(shù)量,發(fā)現(xiàn)獲取不到數(shù)量為5的時(shí)候,原因是線程之間不可見的原因,
// 因?yàn)閠1 添加完對象之后,肯定會更新size 方法,但是在更新size的過程中,t2線程已經(jīng)開始讀了size方法,造成了數(shù)據(jù)不一致
new Thread(() -> {
while (true) {
if (testListAdd1.size() == 5 ) {
break;
}
}
System.out.println("t2 結(jié)束");
},"t2").start();
}
}
結(jié)論:
方法并沒有按預(yù)期的執(zhí)行,這是因?yàn)?,ArraryList的Size方法肯定要更新的,但是還沒有更新完,線程2就讀了,所以這時(shí)候永遠(yuǎn)不會檢測到List的長度為5了,原因就是線程之間的不可見因素
小程序2
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
public class TestListAdd2 {
// 這里用volatile修飾引用類型,發(fā)現(xiàn)并沒有做到線程的之間的可見性,因?yàn)橐脤ο笾赶虻氖且粋€(gè)地址,
// 如果這個(gè)對象的內(nèi)部值被改變了,是無法被觀察到的
volatile List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) {
TestListAdd2 testListAdd2 = new TestListAdd2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
testListAdd2.add(new Object());
System.out.println(i);
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
// t2 線程監(jiān)聽集合的數(shù)量,發(fā)現(xiàn)獲取不到數(shù)量為5的時(shí)候,原因是線程之間不可見的原因,
new Thread(() -> {
while (true) {
if (testListAdd2.size() == 5 ) {
break;
}
}
System.out.println("t2 結(jié)束");
},"t2").start();
}
}
結(jié)論:
小程序2在小程序1的基礎(chǔ)上加上了線程可見volitate修飾,但是還是無法滿足需求,這是因?yàn)関olitate要去修飾普通的值,不要去修飾引用值,因?yàn)樾揎椧妙愋停@個(gè)引用對象指向的是另外一個(gè)new出來的對象,如果這個(gè)對象里邊的成員對象改變了,是無法被觀察到的。
下面自行寫了個(gè)測試修飾引用類型的demo
package com.learn.thread.four.list;
import com.learn.thread.first.T;
public class Test {
private volatile static int a = 0;
// private volatile static Integer a = 0;
public void test() {
a++;
System.out.println(a);
}
public static void main(String[] args) {
Thread[] threads = new Thread[100];
Test test = new Test();
for (int i = threads.length - 1; i >= 0; i--) {
threads[i] = new Thread(() -> test.test());
threads[i].start();
}
}
}
小程序3
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
/**
* 用對象鎖
* wait,和nofity實(shí)現(xiàn)
*/
public class TestListAdd3 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) {
TestListAdd3 testListAdd3 = new TestListAdd3();
final Object lock = new Object();
// t2 要早于t1啟動
new Thread(() -> {
synchronized (lock) {
System.out.println("t2 啟動");
if (testListAdd3.size() != 5 ) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 結(jié)束");
}
},"t2").start();
new Thread(() -> {
synchronized (lock) {
System.out.println("t1 啟動");
for (int i = 0; i < 10; i++) {
testListAdd3.add(new Object());
System.out.println(i);
if (i == 5) {
// 此處有坑
// 特別注意,nofity并不釋放當(dāng)前鎖,t1 會繼續(xù)往下走,等t1 執(zhí)行完了, t2 才會拿到這把對象鎖
lock.notify();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
結(jié)論:
小程序3 也是行不通,原因是nofity方法不釋放鎖,當(dāng)t1線程調(diào)用nofity方法,并沒有釋放當(dāng)前鎖,所以t1還是會繼續(xù)運(yùn)行,等待t1執(zhí)行完畢,t2才會繼續(xù)執(zhí)行,這個(gè)時(shí)候當(dāng)前List不只有5個(gè)了。
小程序4
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
public class TestListAdd4 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) {
TestListAdd4 testListAdd4 = new TestListAdd4();
final Object lock = new Object();
// t2 要早于t1啟動
new Thread(() -> {
synchronized (lock) {
System.out.println("t2 啟動");
if (testListAdd4.size() != 5 ) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 結(jié)束");
// 釋放當(dāng)前鎖,喚醒t1
lock.notify();
}
},"t2").start();
new Thread(() -> {
synchronized (lock) {
System.out.println("t1 啟動");
for (int i = 0; i < 10; i++) {
testListAdd4.add(new Object());
System.out.println(i);
if (i == 5) {
// 此處有坑
// 特別注意,nofity并不釋放當(dāng)前鎖,t1 會繼續(xù)往下走,等t1 執(zhí)行完了, t2 才會拿到這把對象鎖
// 所以下面加多一個(gè)操作,讓此線程等待,阻塞此線程,讓t2 去執(zhí)行
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
結(jié)論
小程序4基本上滿足的需求,在小程序3的基礎(chǔ)上了,nofity之后讓本線程等待,讓給t2執(zhí)行,然后t2執(zhí)行完畢,釋放nofity,回到t1執(zhí)行。
小程序5
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 這里使用CountDown實(shí)現(xiàn)
*/
public class TestListAdd5 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) {
TestListAdd5 testListAdd5 = new TestListAdd5();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 啟動");
if (testListAdd5.size() != 5) {
try {
countDownLatch.await();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("t2 結(jié)束");
}, "t2").start();
new Thread(() -> {
System.out.println("t1 啟動");
for (int i =0; i < 10; i++) {
testListAdd5.add(new Object());
System.out.println(i);
if (testListAdd5.size() == 5) {
// 暫停t1 線程, 打開門閥
countDownLatch.countDown();
}
// 如果這段代碼去除了,就不會給t2時(shí)間去執(zhí)行,所以這里需要等待一會
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1 ").start();
}
}
結(jié)論:
從執(zhí)行結(jié)果來看,并沒有多大的問題,但是如果我們把休眠一秒的代碼去掉,會發(fā)現(xiàn),執(zhí)行結(jié)果不正確,這事因?yàn)閠1線程對象增加到5個(gè)時(shí),t2的線程門閥確實(shí)被打開了,但是t1線程馬上又會接著執(zhí)行,t1之前是休眠1秒,給t2線程執(zhí)行時(shí)間,如果注釋掉這段代碼,t2就沒有機(jī)會去實(shí)時(shí)監(jiān)控了。
小程序6
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class TestListAdd6 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
public static void main(String[] args) throws InterruptedException {
TestListAdd6 testListAdd6 = new TestListAdd6();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 啟動");
if (testListAdd6.size() != 5) {
try {
countDownLatch.await();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("t2 結(jié)束");
}, "t2").start();
new Thread(() -> {
System.out.println("t1 啟動");
for (int i =0; i < 10; i++) {
testListAdd6.add(new Object());
System.out.println(i);
if (testListAdd6.size() == 5) {
// 打開門閥
countDownLatch.countDown();
// 等待
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
結(jié)論
這段代碼很好理解,就是在t1線程打開門閥的時(shí)候,給自己再加一個(gè)門閥,但是這個(gè)線程睡眠代碼,還是不能去除。
小程序7
package com.learn.thread.four.list;
import com.learn.thread.first.T;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
public class TestListAdd7 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
static Thread t2 = null;
static Thread t1 = null;
public static void main(String[] args) {
TestListAdd7 testListAdd7 = new TestListAdd7();
t1 = new Thread(() -> {
System.out.println("t1 啟動了");
for (int i =0; i < 10; i++) {
testListAdd7.add(new Object());
System.out.println(i);
if (testListAdd7.size() == 5) {
LockSupport.unpark(t2);
LockSupport.park();
}
}
},"t1");
t2 = new Thread(() -> {
System.out.println("t2 啟動了");
if (testListAdd7.size() != 5) {
LockSupport.park();
}
System.out.println("t2 結(jié)束");
LockSupport.unpark(t1);
},"t2");
t2.start();
t1.start();
}
}
結(jié)論:
這次跟之前也是大同小異,只不過鎖的方式不一樣罷了
小程序8
package com.learn.thread.four.list;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
public class TestListAdd9 {
private List<Object> list = new ArrayList<Object>(16);
public void add(Object object) {
this.list.add(object);
}
public int size() {
return this.list.size();
}
static Thread t1,t2 = null;
public static void main(String[] args) {
TestListAdd8 testListAdd8 = new TestListAdd8();
Semaphore semaphore = new Semaphore(1);
t1 = new Thread(() -> {
// 保證只有一個(gè)線程執(zhí)行
try {
semaphore.acquire();
for (int i = 0; i < 5; i++) {
testListAdd8.add(new Object());
System.out.println(i);
}
// 釋放當(dāng)前線程,讓別的線程可以加入
semaphore.release();
t2.start();
t2.join();
// 此線程繼續(xù)執(zhí)行
semaphore.acquire();
for (int i = 5; i < 10; i++) {
testListAdd8.add(new Object());
System.out.println(i);
}
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2 = new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 結(jié)束");
semaphore.release();
});
t1.start();
}
}
結(jié)論
本次是一次牽強(qiáng)的方法,用Semaphre限制每次只有一個(gè)線程執(zhí)行,當(dāng)t1增加到4的時(shí)候,釋放當(dāng)前線程,并且將cpu交給t2執(zhí)行,這時(shí)候t2也是保證只有一個(gè)線程執(zhí)行,執(zhí)行完后立馬釋放,t1調(diào)用join完之后可以繼續(xù)獲得Semaphre的鎖,然后繼續(xù)增加對象,最后釋放線程。
針對以上8個(gè)小程序,我們分別用了volitate,wait,nofity,Semphore,CountDownLatch,LockSupport,其中wait和nofity要牢牢掌握
2.3.淘寶面試題2
寫一個(gè)固定容器,擁有put和get方法,以及getCount方法,能夠支持2個(gè)生產(chǎn)者線程以及10個(gè)消費(fèi)者線程阻塞調(diào)用
小程序1
package com.learn.thread.four.prodducer;
import java.util.LinkedList;
public class TestProducer<T> {
private final LinkedList<T> list = new LinkedList<T>();
public final int MAX = 10;
private int count = 0;
public synchronized void put(T t) {
// 想想這里什么用while
// 這是因?yàn)楫?dāng)linkedList集合中的個(gè)數(shù)達(dá)到最大值得時(shí)候,if判斷了集合的大小等于MAX
// 調(diào)用了wait方法,它不會再去判斷一次,而是繼續(xù)往下走,假如wait以后,有別的生產(chǎn)者線程添加數(shù)據(jù),那么,這里就沒有
// 再次判斷,又添加了一次,造成了數(shù)據(jù)錯誤
while (list.size() == MAX) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(t);
++ count;
System.out.println("生產(chǎn)者生產(chǎn) " + count);
// 喚醒所有在隊(duì)列中等待的線程
this.notifyAll();
}
public synchronized T get() {
T t = null;
while (list.size() == 0) {
try {
this.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消費(fèi)");
t = list.removeFirst();
count --;
System.out.println(count);
this.notifyAll();
return t;
}
public static void main(String[] args) {
TestProducer<String> testProducer = new TestProducer<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
System.out.println(testProducer.get());
}
}, "c" + i).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) {
testProducer.put(Thread.currentThread().getName() + "" + j);
}
}, "p" + i).start();
}
}
}
這里有一個(gè)很遺憾的問題,那就是nofityAlll,如果生產(chǎn)者生產(chǎn)完,是有可能喚醒一個(gè)還是生產(chǎn)者的線程,所以這里可以做一個(gè)優(yōu)化
小程序2
package com.learn.thread.four.prodducer;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestProducer2<T> {
private final LinkedList<T> list = new LinkedList<T>();
public final int MAX = 10;
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
Condition producer = lock.newCondition();
Condition comsumer = lock.newCondition();
public void put(T t) {
// 想想這里什么用while
// 這是因?yàn)楫?dāng)linkedList集合中的個(gè)數(shù)達(dá)到最大值得時(shí)候,if判斷了集合的大小等于MAX
// 調(diào)用了wait方法,它不會再去判斷一次,而是繼續(xù)往下走,假如wait以后,有別的生產(chǎn)者線程添加數(shù)據(jù),那么,這里就沒有
// 再次判斷,又添加了一次,造成了數(shù)據(jù)錯誤
try {
lock.lock();
while (list.size() == MAX) {
try {
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(t);
++ count;
System.out.println("生產(chǎn)者生產(chǎn) " + count);
// 喚醒所有消費(fèi)者等待的線程
comsumer.signalAll();
}catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while (list.size() == 0) {
try {
comsumer.await();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消費(fèi)");
t = list.removeFirst();
count --;
System.out.println(count);
producer.signalAll();
}catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
TestProducer2<String> testProducer = new TestProducer2<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
System.out.println(testProducer.get());
}
}, "c" + i).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) {
testProducer.put(Thread.currentThread().getName() + "" + j);
}
}, "p" + i).start();
}
}
}
這里ReentrantLock的優(yōu)勢就體現(xiàn)出來了,它可以有多種情況Condition,在put方法了達(dá)到了峰值就是生產(chǎn)者producer.await,反之就是comsumer.await
ReentrantLock的本質(zhì)就是synchronzied里調(diào)用wait和nofity的時(shí)候,它只有一個(gè)等待隊(duì)列,但是用了Condition就是多個(gè)等待隊(duì)列。當(dāng)我們使用producer.await的時(shí)候,就是進(jìn)入了producer的等待隊(duì)列,producer.signalAll指的是喚醒producer這個(gè)等待隊(duì)列的線程。