面試題:線程A打印1-10數(shù)字,打印到第5個(gè)數(shù)字時(shí),通知線程B
此題考查的是線程間的通信方式。
- 可以利用park/unpark實(shí)現(xiàn)
- 可以利用volatile關(guān)鍵字實(shí)現(xiàn)
- 可以利用synchronized結(jié)合wait notify實(shí)現(xiàn)
- 可以利用JUC中的CountDownLatch實(shí)現(xiàn)
- 可以利用Condition中的await signal 實(shí)現(xiàn)
代碼示例
利用Park/Unpak實(shí)現(xiàn)線程通信
private void notifyThreadWithParkUnpark(){
Thread thb = new Thread("線程B"){
@Override
public void run() {
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"啟動(dòng)了");
}
};
Thread tha =new Thread("線程A"){
@Override
public void run() {
for(int i=1;i<11;i++){
System.out.println(Thread.currentThread().getName()+i);
if(i==5){
LockSupport.unpark(thb);
}
}
}
};
thb.start();
tha.start();
}
park與unpark可以看做一個(gè)令牌,park就是等待令牌,unpark就是頒發(fā)一個(gè)令牌,另外需要注意的是park與unpark的調(diào)用次數(shù)不用一一對(duì)應(yīng),而且假如在同步代碼塊中調(diào)用park方法,線程會(huì)進(jìn)入阻塞狀態(tài),但是不會(huì)釋放已經(jīng)占用的鎖。
本例使用park使線程B進(jìn)入阻塞等待狀態(tài),在線程A調(diào)用unpark并傳入線程B的名稱使線程B可以繼續(xù)運(yùn)行。
使用Volatile關(guān)鍵字實(shí)現(xiàn)線程通信
private static volatile boolean flag = false;
private void notifyThreadWithVolatile(){
Thread thc= new Thread("線程C"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(i==5){
flag=true;
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+i);
}
}
};
Thread thd= new Thread("線程D"){
@Override
public void run() {
while (true){
// 防止偽喚醒 所以使用了while
while(flag){
System.out.println(Thread.currentThread().getName()+"收到通知");
break;
}
}
}
};
thd.start();
try {
Thread.sleep(1000L);
} catch (Exception e) {
e.printStackTrace();
}
thc.start();
}
volatile表示的禁用CPU緩存,用volatile修飾的變量,會(huì)強(qiáng)制從主內(nèi)存中讀取變量的值。java內(nèi)存模型中關(guān)于volatile也是有說明的,volatile只能保證可見性,但不能保證原子性。
本例通過在volatile來修飾一個(gè)標(biāo)志位,線程C修改了該標(biāo)志位,然后線程D就可以“看到”標(biāo)志位的修改,從而實(shí)現(xiàn)互相通信。
使用Synchronized 集合wait notify實(shí)現(xiàn)線程間通信
private static final Object lock = new Object();
private void notifyThreadWithSynchronized(){
Thread the = new Thread("線程E"){
@Override
public void run() {
synchronized (lock){
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
if(i==5){
lock.notify();
}
}
}
}
};
Thread thf = new Thread("線程F"){
@Override
public void run() {
while(true){
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"啟動(dòng)了");
}
}
}
};
thf.start();
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
the.start();
}
synchronized修飾同步代碼塊,而wait notify notify必須是在synchronized修飾代碼塊中使用,否則會(huì)拋出監(jiān)視器異常。
本實(shí)例定義一個(gè)對(duì)象鎖,而線程F首先獲取到互斥鎖,在執(zhí)行wait()方法時(shí),釋放已經(jīng)持有的互斥鎖,進(jìn)入等待隊(duì)列。而線程E執(zhí)行獲取到互斥鎖開始執(zhí)行,當(dāng)1==5時(shí),調(diào)用notify方法,就會(huì)通知lock的等待隊(duì)列,然后線程E會(huì)繼續(xù)執(zhí)行,由于線程F此時(shí)還是獲取不到互斥鎖(因?yàn)楸痪€程E占用),所以會(huì)在線程E執(zhí)行完畢后,才能獲取到執(zhí)行權(quán)。
利用CountDonwLatch實(shí)現(xiàn)線程間通信
// 倒計(jì)時(shí)器
private CountDownLatch cdl = new CountDownLatch(1);
private void notifyThreadWithCountDownLatch(){
Thread thg = new Thread("線程G"){
@Override
public void run() {
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"啟動(dòng)了");
}
};
thg.start();
Thread thh = new Thread("線程H"){
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println(Thread.currentThread().getName()+i);
if(i==5){
cdl.countDown();
}
}
}
};
thh.start();
}
本示例中使用了CountDownLatch倒計(jì)時(shí)器,利用了倒計(jì)時(shí)器的阻塞特性來實(shí)現(xiàn)等待。具體就是聲明一個(gè)計(jì)數(shù)器為1的倒計(jì)時(shí)器,線程G調(diào)用await()方法進(jìn)入等待,直到計(jì)數(shù)器為0的時(shí)候才能夠進(jìn)入執(zhí)行,而線程H在i==5會(huì)將計(jì)數(shù)器減一,使其為0,此時(shí)線程G就會(huì)繼續(xù)執(zhí)行了。
利用Condition中的await和signal來實(shí)現(xiàn)
// ReentrantLock+ condition
private Lock rtl=new ReentrantLock();
private Condition condition = rtl.newCondition();
private void notifyThreadWithCondition(){
Thread thi = new Thread("線程I"){
@Override
public void run() {
while (true){
rtl.lock();
try {
condition.await();
System.out.println(Thread.currentThread().getName()+"啟動(dòng)了");
break;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rtl.unlock();
}
}
}
};
Thread thj = new Thread("線程J"){
@Override
public void run() {
rtl.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
if(i==5){
condition.signal();
}
}
} finally {
rtl.unlock();
}
}
};
thi.start();
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
thj.start();
}
本示例是結(jié)合ReentrantLock和Condition來進(jìn)行控制線程間的執(zhí)行順序,Condition的await()和signal(),他們的語義和wait notify是一樣的。區(qū)別是在synchronized代碼塊里調(diào)用wait notify。通過示例可以看到這中方法實(shí)現(xiàn)會(huì)不斷的加鎖與解鎖,所以看起來稍微復(fù)雜些。
總結(jié)
通過以上代碼看到通過volatile的方式是最簡(jiǎn)潔方便,用park與unpark方式是比較靈活,不用加鎖或解鎖,剩下的synchronized與Conditon都是用了鎖,而CountDownLatch則是利用了計(jì)數(shù)器。