7、多線程面試題
1.多線程的創(chuàng)建方式
(1)繼承Thread類(lèi):但Thread本質(zhì)上也是實(shí)現(xiàn)了Runnable接口的一個(gè)實(shí)例,它代表一個(gè)線程的實(shí)例,并且,啟動(dòng)線程的唯一方法就是通過(guò)Thread類(lèi)的start()實(shí)例方法。start()方法是一個(gè)native方法,它將啟動(dòng)一個(gè)新線程,并執(zhí)行run()方法。這種方式實(shí)現(xiàn)多線程很簡(jiǎn)單,通過(guò)自己的類(lèi)直接extend Thread,并復(fù)寫(xiě)run()方法,就可以啟動(dòng)新線程并執(zhí)行自己定義的run()方法。例如:繼承Thread類(lèi)實(shí)現(xiàn)多線程,并在合適的地方啟動(dòng)線程。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
(2)實(shí)現(xiàn)Runnable接口的方式實(shí)現(xiàn)多線程,并且實(shí)例化Thread,傳入自己的Thread實(shí)例,調(diào)用run()方法。
public class MyThread implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
(3)使用ExecutorService、Callable、Future實(shí)現(xiàn)有返回結(jié)果的多線程:ExecutorService、Callable、Future這個(gè)對(duì)象實(shí)際上都是屬于Executor框架中的功能類(lèi)。返回結(jié)果的線程是在JDK1.5中引入的新特征,確實(shí)很實(shí)用,有了這種特征我就不需要再為了得到返回值而大費(fèi)周折了,而且即便實(shí)現(xiàn)了也可能漏洞百出??煞祷刂档娜蝿?wù)必須實(shí)現(xiàn)Callable接口,類(lèi)似的,無(wú)返回值的任務(wù)必須實(shí)現(xiàn)Runnable接口。執(zhí)行Callable任務(wù)后,可以獲取一個(gè)Future的對(duì)象,在該對(duì)象上調(diào)用get就可以獲取到Callable任務(wù)返回的Object了,再結(jié)合線程池接口ExecutorService就可以實(shí)現(xiàn)有返回結(jié)果的多線程了。下面提供了一個(gè)完整的有返回結(jié)果的多線程測(cè)試?yán)?,在JDK1.5下驗(yàn)證過(guò)沒(méi)問(wèn)題可以直接使用。代碼如下:
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException{
System.out.println("----程序開(kāi)始運(yùn)行----");
Date date1 = new Date();
int taskSize = 5;
// 創(chuàng)建一個(gè)線程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 創(chuàng)建多個(gè)有返回值的任務(wù)
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 執(zhí)行任務(wù)并獲取 Future 對(duì)象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 關(guān)閉線程池
pool.shutdown();
// 獲取所有并發(fā)任務(wù)的運(yùn)行結(jié)果
for (Future f : list) {
// 從 Future 對(duì)象上獲取任務(wù)的返回值,并輸出到控制臺(tái)
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序結(jié)束運(yùn)行----,程序運(yùn)行時(shí)間【" + (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任務(wù)啟動(dòng)");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任務(wù)終止");
return taskNum + "任務(wù)返回運(yùn)行結(jié)果,當(dāng)前任務(wù)時(shí)間【" + time + "毫秒】";
}
}
2.在java中wait和sleep方法的不同?
最大的不同是在等待時(shí)wait會(huì)釋放鎖,而sleep一直持有鎖。wait通常被用于線程間交互,sleep通常被用于暫停執(zhí)行。
3.synchronized和volatile關(guān)鍵字的作用?
一旦一個(gè)共享變量(類(lèi)的成員變量、類(lèi)的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語(yǔ)義:
● 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的。
● 禁止進(jìn)行指令重排序。
● volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問(wèn)該變量,其他線程被阻塞住。
● volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類(lèi)級(jí)別的。
● volatile僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,并不能保證原子性;synchronized則可以保證變量的修改可見(jiàn)性和原子性。
● volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
4.分析線程并發(fā)訪問(wèn)代碼解釋原因?
public class Counter {
private volatile int count = 0;
public void inc() {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
@Override
public String toString() {
return "[count=" + count + "]";
}
}
public class VolatileTest {
public static void main(String[] args) {
final Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.inc();
}
}).start();
}
System.out.println(counter);
}
}
上面的代碼執(zhí)行完后輸出的結(jié)果確定為1000嗎?答案是不一定,或者不等于 1000。你知道這是為什么嗎?
在java的內(nèi)存模型中每一個(gè)線程運(yùn)行時(shí)都有一個(gè)線程棧,線程棧保存了線程運(yùn)行時(shí)候變量值信息。當(dāng)線程訪問(wèn)某一個(gè)對(duì)象時(shí)候值的時(shí)候,首先通過(guò)對(duì)象的引用找到對(duì)應(yīng)在堆內(nèi)存的變量的值,然后把堆內(nèi)存變量的具體值load到線程本地內(nèi)存中,建立一個(gè)變量副本,之后線程就不再和對(duì)象在堆內(nèi)存變量值有任何關(guān)系,而是直接修改副本變量的值,在修改完之后的某一個(gè)時(shí)刻(線程退出之前),自動(dòng)把線程變量副本的值回寫(xiě)到對(duì)象在堆中變量。這樣在堆中的對(duì)象的值就產(chǎn)生變化了。
也就是說(shuō)上面主函數(shù)中開(kāi)啟了1000個(gè)子線程,每個(gè)線程都有一個(gè)變量副本,每個(gè)線程修改變量只是臨時(shí)修改了自己的副本,當(dāng)線程結(jié)束時(shí)再將修改的值寫(xiě)入在主內(nèi)存中,這樣就出現(xiàn)了線程安全問(wèn)題。因此結(jié)果就不可能等于1000了,一般都會(huì)小于1000。
上面的解釋用一張圖表示如下:
5.什么是線程池,如何使用?
線程池就是事先將多個(gè)線程對(duì)象放到一個(gè)容器中,當(dāng)使用的時(shí)候就不用new線程而是直接去池中拿線程即可,節(jié)省了開(kāi)辟子線程的時(shí)間,提高的代碼執(zhí)行效率。在JDK的java.util.concurrent.Executors中提供了生成多種線程池的靜態(tài)方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后調(diào)用他們的 execute 方法即可。
6.常用的線程池有哪些?
● newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池,此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。
● newFixedThreadPool:創(chuàng)建固定大小的線程池,每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大小。
● newCachedThreadPool:創(chuàng)建一個(gè)可緩存的線程池,此線程池不會(huì)對(duì)線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說(shuō)JVM)能夠創(chuàng)建的最大線程大小。
● newScheduledThreadPool:創(chuàng)建一個(gè)大小無(wú)限的線程池,此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
● newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
7. 請(qǐng)敘述一下您對(duì)線程池的理解?
(如果問(wèn)到了這樣的問(wèn)題,可以展開(kāi)的說(shuō)一下線程池如何用、線程池的好處、線程池的啟動(dòng)策略)合理利用線程池能夠帶來(lái)三個(gè)好處。
第一:降低資源消耗。通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷(xiāo)毀造成的消耗。
第二:提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
第三:提高線程的可管理性。線程是稀缺資源,如果無(wú)限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
1、線程池剛創(chuàng)建時(shí),里面沒(méi)有一個(gè)線程。任務(wù)隊(duì)列是作為參數(shù)傳進(jìn)來(lái)的。不過(guò),就算隊(duì)列里面有任務(wù),線程池也不會(huì)馬上執(zhí)行它們。
2、當(dāng)調(diào)用execute()方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判斷:
(1)如果正在運(yùn)行的線程數(shù)量小于corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);
(2)如果正在運(yùn)行的線程數(shù)量大于或等于corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列;
(3)如果這時(shí)候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于maximumPoolSize,那么還是要?jiǎng)?chuàng)建線程運(yùn)行這個(gè)任務(wù);
(4)如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會(huì)拋出異常,告訴調(diào)用者“我不能再接受任務(wù)了”。
(5)當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來(lái)執(zhí)行。
(6)當(dāng)一個(gè)線程無(wú)事可做,超過(guò)一定的時(shí)間(keepAliveTime)時(shí),線程池會(huì)判斷,如果當(dāng)前運(yùn)行的線程數(shù)大于corePoolSize,那么這個(gè)線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會(huì)收縮到corePoolSize的大小。
1.package com.bjpowernode;
2.
3.import java.util.concurrent.Semaphore;
4. /**
5. *
6. * @author dujubin
7. *
8. */
9. public class SemaphoreTest {
10./*
11.* permits the initial number of permits available. This value may be negative,
12.in which case releases must occur before any acquires will be granted.
13.fair true if this semaphore will guarantee first-in first-out granting of
14.permits under contention, else false
15. */
16. static Semaphore semaphore = new Semaphore(5,true);
17. public static void main(String[] args) {
18. for (int i = 0; i < 100; i++) {
19. new Thread(new Runnable()
20. {
21. @Override
22. public void run () {
23. test();
24. }
25. }).start();
26. }
27.
28. }
29.
30. public static void test() {
31. try {
32. //申請(qǐng)一個(gè)請(qǐng)求
33. semaphore.acquire();
34. } catch (InterruptedException e1) {
35. e1.printStackTrace();
36. }
37. System.out.println(Thread.currentThread().getName() + "進(jìn)來(lái)了");
38. try {
39. Thread.sleep(1000);
40. } catch (InterruptedException e) {
41. e.printStackTrace();
42. }
43. System.out.println(Thread.currentThread().getName() + "走了");
44. //釋放一個(gè)請(qǐng)求
45. semaphore.release();
46. }
47.}
可以使用Semaphore控制,第16行的構(gòu)造函數(shù)創(chuàng)建了一個(gè)Semaphore對(duì)象,并且初始化了5個(gè)信號(hào)。這樣的效果是控件test方法最多只能有5個(gè)線程并發(fā)訪問(wèn),對(duì)于5個(gè)線程時(shí)就排隊(duì)等待,走一個(gè)來(lái)一下。第33行,請(qǐng)求一個(gè)信號(hào)(消費(fèi)一個(gè)信號(hào)),如果信號(hào)被用完了則等待,第45行釋放一個(gè)信號(hào),釋放的信號(hào)新的線程就可以使用了。
根據(jù)問(wèn)題的描述,我將問(wèn)題用以下代碼演示,ThreadA、ThreadB、ThreadC,ThreadA用于初始化數(shù)據(jù)num,只有當(dāng)num初始化完成之后再讓ThreadB和ThreadC獲取到初始化后的變量num。分析過(guò)程如下:
考慮到多線程的不確定性,因此我們不能確保ThreadA就一定先于ThreadB和ThreadC前執(zhí)行,就算ThreadA先執(zhí)行了,我們也無(wú)法保證ThreadA什么時(shí)候才能將變量num給初始化完成。因此我們必須讓ThreadB和ThreadC去等待ThreadA完成任何后發(fā)出的消息。
現(xiàn)在需要解決兩個(gè)難題,一是讓ThreadB和ThreadC等待ThreadA先執(zhí)行完,二是ThreadA執(zhí)行完之后給ThreadB和ThreadC發(fā)送消息。
解決上面的難題我能想到的兩種方案,一是使用純Java API的Semaphore類(lèi)來(lái)控制線程的等待和釋放,二是使用Android提供的Handler消息機(jī)制。
解決方案一:
1. package com.bjpowernode;
2. /**
3. * 三個(gè)線程 a、b、c 并發(fā)運(yùn)行,b,c 需要 a 線程的數(shù)據(jù)怎么實(shí)現(xiàn)
4. *
5. */
6.public class ThreadCommunication {
7. private static int num;//定義一個(gè)變量作為數(shù)據(jù)8.
9. public static void main(String[] args) {
10.
11. Thread threadA = new Thread(new Runnable() {
12.
13. @Override
14. public void run() {
15. try {
16. //模擬耗時(shí)操作之后初始化變量 num
17. Thread.sleep(1000);
18. num = 1;
19.
20. } catch (InterruptedException e) {
21. e.printStackTrace();
22. }
23. }
24. });
25. Thread threadB = new Thread(new Runnable() {
26.
27. @Override
28. public void run() {
29. System.out.println(Thread.currentThread().getName()+"獲取到 num 的值為:"+num);
30. }
31. });
32. Thread threadC = new Thread(new Runnable() {
33.
34. @Override
35. public void run() {
36. System.out.println(Thread.currentThread().getName()+"獲取到 num 的值為:"+num);
37. }
38. });
39. //同時(shí)開(kāi)啟 3 個(gè)線程
40. threadA.start();
41. threadB.start();
42. threadC.start();
43.
44. }
45. }
46.
解決方案二:
ublic class ThreadCommunication {
private static int num;
/**
* 定義一個(gè)信號(hào)量,該類(lèi)內(nèi)部維持了多個(gè)線程鎖,可以阻塞多個(gè)線程,釋放多個(gè)線程,
* 線程的阻塞和釋放是通過(guò) permit 概念來(lái)實(shí)現(xiàn)的
* 線程通過(guò) semaphore.acquire()方法獲取 permit,如果當(dāng)前 semaphore 有 permit 則分配給該線程,
* 如果沒(méi)有則阻塞該線程直到 semaphore
* 調(diào)用 release()方法釋放 permit。
* 構(gòu)造函數(shù)中參數(shù):permit(允許) 個(gè)數(shù),
*/
private static Semaphore semaphore = new Semaphore(0);
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
//模擬耗時(shí)操作之后初始化變量 num
Thread.sleep(1000);
num = 1;
//初始化完參數(shù)后釋放兩個(gè) permit
semaphore.release(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
//獲取 permit,如果 semaphore 沒(méi)有可用的 permit 則等待,如果有則消耗一個(gè)
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "獲取到 num 的值為:" + num);
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
//獲取 permit,如果 semaphore 沒(méi)有可用的 permit 則等待,如果有則消耗一個(gè)
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "獲取到 num 的值為:" + num);
}
});
//同時(shí)開(kāi)啟 3 個(gè)線程
threadA.start();
threadB.start();
threadC.start();
}
}