一 用Runnable還是Thread?
Java中實現(xiàn)多線程有兩種方法:繼承Thread類、實現(xiàn)Runnable接口,在程序開發(fā)中只要是多線程,肯定永遠以實現(xiàn)Runnable接口為主姐帚,因為實現(xiàn)Runnable接口相比繼承Thread類有如下優(yōu)勢:
- 可以避免由于Java的單繼承特性而帶來的局限;
- 增強程序的健壯性障涯,代碼能夠被多個線程共享罐旗,代碼與數(shù)據(jù)是獨立的膳汪;
- 適合多個相同程序代碼的線程區(qū)處理同一資源的情況(資源共享);
二 Thread 類中的start() 和 run() 方法有什么區(qū)別九秀?
- start()方法來啟動線程遗嗽,真正實現(xiàn)了多線程運行。調(diào)用start()后颤霎,線程會被放到等待隊列媳谁,等待CPU調(diào)度,并不一定要馬上開始執(zhí)行友酱,只是將這個線程置于就緒狀態(tài)晴音。然后通過JVM,線程Thread會調(diào)用run()方法缔杉,執(zhí)行本線程的線程體锤躁。所以執(zhí)行start()方法后可以繼續(xù)執(zhí)行后面的代碼,不會阻塞或详。
- run()方法當作普通方法的方式調(diào)用系羞。程序還是要順序執(zhí)行,要等待run方法體執(zhí)行完畢后霸琴,才可繼續(xù)執(zhí)行下面的代碼椒振; 程序中只有主線程——這一個線程迷帜, 其程序執(zhí)行路徑還是只有一條丐黄, 這樣就沒有達到寫線程的目的。
public class Jyy {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runner1());
Thread thread2 = new Thread(new Runner2());
// thread1.start();
// thread2.start();
thread1.run();
thread2.run();
}
}
class Runner1 implements Runnable {
public void run() {
System.out.println("進入Runner1運行狀態(tài)");
}
}
class Runner2 implements Runnable {
public void run() {
System.out.println("進入Runner2運行狀態(tài)");
}
}
執(zhí)行run()方法就像普通方法一樣衅檀,沒有創(chuàng)建新線程选调,總是按順序執(zhí)行:
進入Runner1運行狀態(tài)
進入Runner2運行狀態(tài)
Process finished with exit code 0
執(zhí)行start()方法就會創(chuàng)建新線程夹供,等待CPU進行調(diào)度異步執(zhí)行:
進入Runner2運行狀態(tài)
進入Runner1運行狀態(tài)
Process finished with exit code 0
三 Java中CyclicBarrier 和 CountDownLatch有什么不同?
1 等待多線程完成的CountDownLatch
CountDownLatch與許一個或多個線程等待其他線程完成操作仁堪。例如要解析一個Excel中的多個sheet表哮洽,等到所有sheet表都解析完之后,程序提示解析完成弦聂。
public class Jyy {
/**
* parse1先執(zhí)行鸟辅,parse2后執(zhí)行
* @param args
*/
public static void main(String[] args) {
Thread parse1 = new Thread(new Runnable() {
public void run() {
System.out.println("parse1");
}
});
Thread parse2 = new Thread(new Runnable() {
public void run() {
try {
parse1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("parse2");
}
});
parse1.start();
parse2.start();
try {
parse2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("finished");
}
}
程序會按上述邏輯進行,輸出結(jié)果為:
parse1
parse2
finished
join()用于讓當前線程等待調(diào)用join()的線程執(zhí)行結(jié)束横浑。其原理是不停的檢查調(diào)用join()的線程t是否存活剔桨,如果線程t一直存活則當前線程會永遠等待。直到j(luò)oin()線程終止后徙融,線程的notifyAll()會被調(diào)用。join()內(nèi)部實現(xiàn)如下:
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
CountDownLatch也可以實現(xiàn)join()功能瑰谜。
public class Jyy {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread parse1 = new Thread(new Runnable() {
public void run() {
System.out.println("parse1");
countDownLatch.countDown();
}
});
Thread parse2 = new Thread(new Runnable() {
public void run() {
System.out.println("parse2");
countDownLatch.countDown();
}
});
parse1.start();
parse2.start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("finished");
}
}
CountDownLatch的構(gòu)造函數(shù)接收一個int類型的參數(shù)作為計數(shù)器欺冀,如果你想等待N個點完成树绩,這里就傳入N。
當我們調(diào)用一次CountDownLatch的countDown方法時隐轩,N就會減1饺饭,CountDownLatch的await會阻塞當前線程,直到N變成零职车。由于countDown方法可以用在任何地方瘫俊,所以這里說的N個點,可以是N個線程悴灵,也可以是1個線程里的N個執(zhí)行步驟扛芽。用在多個線程時,你只需要把這個CountDownLatch的引用傳遞到線程里积瞒。
注意:計數(shù)器必須大于等于0川尖,只是等于0時候,計數(shù)器就是零茫孔,調(diào)用await方法時不會阻塞當前線程叮喳。CountDownLatch不可能重新初始化或者修改CountDownLatch對象的內(nèi)部計數(shù)器的值。一個線程調(diào)用countDown方法 happen-before 另外一個線程調(diào)用await方法缰贝。
2 同步屏障CyclicBarrier
CyclicBarrier 的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)馍悟。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞剩晴,直到最后一個線程到達屏障時锣咒,屏障才會開門,所有被屏障攔截的線程才會繼續(xù)干活李破。CyclicBarrier默認的構(gòu)造方法是CyclicBarrier(int parties)宠哄,其參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用await方法告訴CyclicBarrier我已經(jīng)到達了屏障嗤攻,然后當前線程被阻塞毛嫉。
public class CyclicBarrierTest {
static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
c.await();
} catch (Exception e) {
}
System.out.println(1);
}
}).start();
try {
c.await();
} catch (Exception e) {
}
System.out.println(2);
}
}
如果把new CyclicBarrier(2)修改成new CyclicBarrier(3)則主線程和子線程會永遠等待,因為沒有第三個線程執(zhí)行await方法妇菱,即沒有第三個線程到達屏障承粤,所以之前到達屏障的兩個線程都不會繼續(xù)執(zhí)行。
CyclicBarrier還提供一個更高級的構(gòu)造函數(shù)CyclicBarrier(int parties, Runnable barrierAction)闯团,用于在線程到達屏障時辛臊,優(yōu)先執(zhí)行barrierAction,方便處理更復雜的業(yè)務(wù)場景房交。代碼如下:
public class Jyy {
static CyclicBarrier c = new CyclicBarrier(2, new A());
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
c.await();
} catch (Exception e) {
}
System.out.println(1);
}
}).start();
try {
c.await();
} catch (Exception e) {
}
System.out.println(2);
}
static class A implements Runnable {
@Override
public void run() {
System.out.println(3);
}
}
}
輸出結(jié)果:
3 2 1
或 3 1 2
CyclicBarrier的應用場景
CyclicBarrier可以用于多線程計算數(shù)據(jù)彻舰,最后合并計算結(jié)果的應用場景。比如我們用一個Excel保存了用戶所有銀行流水,每個Sheet保存一個帳戶近一年的每筆銀行流水刃唤,現(xiàn)在需要統(tǒng)計用戶的日均銀行流水隔心,先用多線程處理每個sheet里的銀行流水,都執(zhí)行完之后尚胞,得到每個sheet的日均銀行流水硬霍,最后,再用barrierAction用這些線程的計算結(jié)果笼裳,計算出整個Excel的日均銀行流水唯卖。
3 CyclicBarrier和CountDownLatch的區(qū)別
- CountDownLatch的計數(shù)器只能使用一次。而CyclicBarrier的計數(shù)器可以使用reset() 方法重置躬柬。所以CyclicBarrier能處理更為復雜的業(yè)務(wù)場景拜轨,比如如果計算發(fā)生錯誤,可以重置計數(shù)器楔脯,并讓線程們重新執(zhí)行一次撩轰。
- CountDownLatch強調(diào)的是一個線程(或多個)需要等待另外的n個線程干完某件事情之后才能繼續(xù)執(zhí)行。 CyclicBarrier強調(diào)的是n個線程昧廷,大家相互等待堪嫂,只要有一個沒完成,所有人都得等著木柬。
一個更加形象的例子參見文章盡量把CyclicBarrier和CountDownLatch的區(qū)別說通俗點
四 一個線程運行時發(fā)生異常會怎樣皆串?
簡單的說,如果異常沒有被捕獲該線程將會停止執(zhí)行眉枕。Thread.UncaughtExceptionHandler是用于處理未捕獲異常造成線程突然中斷情況的一個內(nèi)嵌接口恶复。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler并將線程和異常作為參數(shù)傳遞給handler的uncaughtException()方法進行處理。
五 Java中如何停止一個線程速挑?
1 拋出InterruptedException 運行時異常法
調(diào)用線程的 interrupt()方法并檢測中斷狀態(tài)谤牡,拋出一個InterruptedException,捕獲該異常并進行退出邏輯姥宝。
public class Jyy {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.interrupt();
}
public static class MyThread extends Thread {
public void run() {
try {
for(int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
throw new InterruptedException();
}
System.out.println(i);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("exit");
}
}
}
}
2 return法
線程被中斷后翅萤,直接調(diào)用return返回。
public class Jyy {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.interrupt();
}
public static class MyThread extends Thread {
public void run() {
for(int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
return;
}
System.out.println(i);
}
}
}
}
3 中斷標識位
設(shè)置一個volatile變量作為中斷標識位腊满,當調(diào)用cancel()方法將標識位設(shè)為false時套么,線程結(jié)束。
public class Jyy {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task);
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
task.cancel();
}
public static class MyTask implements Runnable {
private volatile boolean on = true;
public void run() {
int i = 0;
while (on) {
System.out.println(i++);
}
}
public void cancel() {
on = false;
}
}
}
六 Java中notify 和 notifyAll有什么區(qū)別碳蛋?
notify()方法不能喚醒某個具體的線程胚泌,所以只有一個線程在等待的時候它才有用武之地。而notifyAll()喚醒所有線程并允許他們爭奪鎖確保了至少有一個線程能繼續(xù)運行肃弟。
七 為什么wait, notify 和 notifyAll這些方法不在thread類里面玷室?
一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的零蓉,每個對象都有鎖,通過線程獲得阵苇。如果線程需要等待某些鎖那么調(diào)用對象中的wait()方法就有意義了壁公。如果wait()方法定義在Thread類中感论,線程正在等待的是哪個鎖就不明顯了绅项。簡單的說,由于wait比肄,notify和notifyAll都是鎖級別的操作快耿,所以把他們定義在Object類中因為鎖屬于對象。
八 Java中interrupted 和 isInterruptedd方法的區(qū)別芳绩?
interrupted() 和 isInterrupted()的主要區(qū)別是前者會將中斷狀態(tài)清除而后者不會掀亥。Java多線程的中斷機制是用內(nèi)部標識來實現(xiàn)的,調(diào)用Thread.interrupt()來中斷一個線程就會設(shè)置中斷標識為true妥色。當中斷線程調(diào)用靜態(tài)方法Thread.interrupted()來檢查中斷狀態(tài)時搪花,中斷狀態(tài)會被清零。
非靜態(tài)方法isInterrupted()用來查詢其它線程的中斷狀態(tài)且不會改變中斷狀態(tài)標識嘹害。 簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態(tài)清零撮竿。無論如何,一個線程的中斷狀態(tài)有有可能被其它線程調(diào)用中斷來改變笔呀。
九 什么是線程池幢踏? 為什么要使用它?
參見文章Java線程池解析
十 死鎖
參見文章死鎖詳述
十一 怎么檢測一個線程是否擁有鎖许师?
在java.lang.Thread中有一個方法叫holdsLock()房蝉,它返回true如果當且僅當當前線程擁有某個具體對象的鎖。
十二 Thread類中的yield方法有什么作用微渠?
Yield方法可以暫停當前正在執(zhí)行的線程對象搭幻,讓其它有相同優(yōu)先級的線程執(zhí)行。它是一個靜態(tài)方法而且只保證當前線程放棄CPU占用而不能保證使其它線程一定能占用CPU逞盆,執(zhí)行yield()的線程有可能在進入到暫停狀態(tài)后馬上又被執(zhí)行檀蹋。
十三 單例模式的雙檢鎖是什么?
參見文章雙重檢查機制被破解的聲明
十四 如何在Java中創(chuàng)建線程安全的Singleton纳击?
參見文章 完美的單例模式
十五 線程釋放鎖的時機
- 執(zhí)行完同步代碼塊
- 在執(zhí)行同步代碼塊的過程中续扔,遇到異常而導致線程終止
- 在執(zhí)行同步代碼塊的過程中,執(zhí)行了鎖所屬對象的wait()方法焕数,這個線程會釋放鎖纱昧,進行對象的等待池
十六 什么是上下文切換?
上下文切換(有時也稱做進程切換或任務(wù)切換)是指 CPU 從一個進程或線程切換到另一個進程或線程堡赔。詳情參見文章上下文切換詳解
如何減少上下文切換识脆?
- 無鎖并發(fā)編程
多線程競爭鎖時會引起上下文切換,所以多線程處理數(shù)據(jù)時,可以將數(shù)據(jù)的ID利用hash算法取模分段灼捂,不同的線程處理不同的分段 - CAS
- 使用最少線程
- 協(xié)程
在單線程里實現(xiàn)多任務(wù)的調(diào)度离例,并在單線程中維持多任務(wù)的切換
十七 線程狀態(tài)
十八 控制并發(fā)線程數(shù)的Semaphore
簡介
Semaphore(信號量)是用來控制同時訪問特定資源的線程數(shù)量,它通過協(xié)調(diào)各個線程悉稠,以保證合理的使用公共資源宫蛆。很多年以來,我都覺得從字面上很難理解Semaphore所表達的含義的猛,只能把它比作是控制流量的紅綠燈耀盗,比如XX馬路要限制流量,只允許同時有一百輛車在這條路上行使卦尊,其他的都必須在路口等待叛拷,所以前一百輛車會看到綠燈,可以開進這條馬路岂却,后面的車會看到紅燈忿薇,不能駛?cè)隭X馬路,但是如果前一百輛中有五輛車已經(jīng)離開了XX馬路躏哩,那么后面就允許有5輛車駛?cè)腭R路署浩,這個例子里說的車就是線程,駛?cè)腭R路就表示線程在執(zhí)行震庭,離開馬路就表示線程執(zhí)行完成瑰抵,看見紅燈就表示線程被阻塞,不能執(zhí)行器联。
應用場景
Semaphore可以用于做流量控制二汛,特別公用資源有限的應用場景,比如數(shù)據(jù)庫連接拨拓。假如有一個需求肴颊,要讀取幾萬個文件的數(shù)據(jù),因為都是IO密集型任務(wù)渣磷,我們可以啟動幾十個線程并發(fā)的讀取婿着,但是如果讀到內(nèi)存后,還需要存儲到數(shù)據(jù)庫中醋界,而數(shù)據(jù)庫的連接數(shù)只有10個竟宋,這時我們必須控制只有十個線程同時獲取數(shù)據(jù)庫連接保存數(shù)據(jù),否則會報錯無法獲取數(shù)據(jù)庫連接形纺。這個時候丘侠,我們就可以使用Semaphore來做流控,代碼如下:
public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors
.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (InterruptedException e) {
}
}
});
}
threadPool.shutdown();
}
}
在代碼中逐样,雖然有30個線程在執(zhí)行蜗字,但是只允許10個并發(fā)的執(zhí)行打肝。Semaphore的構(gòu)造方法Semaphore(int permits) 接受一個整型的數(shù)字,表示可用的許可證數(shù)量挪捕。Semaphore(10)表示允許10個線程獲取許可證粗梭,也就是最大并發(fā)數(shù)是10。Semaphore的用法也很簡單级零,首先線程使用Semaphore的acquire()獲取一個許可證断医,使用完之后調(diào)用release()歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證妄讯。
其他方法
Semaphore還提供一些其他方法:
int availablePermits() :返回此信號量中當前可用的許可證數(shù)孩锡。
int getQueueLength():返回正在等待獲取許可證的線程數(shù)。
boolean hasQueuedThreads() :是否有線程正在等待獲取許可證亥贸。
void reducePermits(int reduction) :減少reduction個許可證。
Collection getQueuedThreads() :返回所有等待獲取許可證的線程集合浇垦。
十九 在單核CPU中炕置,synchronized有意義嗎?
在單核CPU中男韧,多線程是通過競爭CPU時間片并發(fā)執(zhí)行不同線程朴摊,這使得使用者感覺到多線程在并發(fā)執(zhí)行任務(wù)。
Java具有自己的內(nèi)存模型此虑,詳見Java內(nèi)存模型甚纲。線程的共享變量存儲在主存中,線程還有自己的本地內(nèi)存朦前,線程對共享變量進行操作時先要將共享變量存儲在本地內(nèi)存中介杆,之后對本地內(nèi)存中的共享變量副本就行操作,操作完成后將共享變量更新到主存中韭寸。
由此可以看出線程操作共享變量val一共分為三步:
- 保存val的副本于本地內(nèi)存
- 操作val副本
- 更新val到主內(nèi)存
假設(shè)多線程對共享變量val進行操作春哨,線程1在一個CPU時間片內(nèi)完成了1、2步的操恩伺,緊接著線程2獲取了CPU時間片并對val進行操作赴背,由于線程1沒有執(zhí)行第3步所以線程2在第1步讀取到的val仍舊為原值,并沒有感知到線程1對val進行的改變晶渠。
總結(jié):Java內(nèi)存模型是對CPU內(nèi)存模型的一個抽象凰荚,它對于CPU是否是多核不敏感,所以synchronized仍然發(fā)揮其可見性和互斥性語義褒脯。