1線程基本操作
1.1 創(chuàng)建線程
- 1.繼承java.lang.Thread
public class MyThread extends Thread {
//復(fù)寫run方法.
public void run() {
//...
}
}
public class MultiThread {
public static void main(String[] args) {
MyThread t = new MyThread();
//調(diào)用start 來 啟動子線程
t.start();
//主線程繼續(xù)同時(shí)向下執(zhí)行
//...
}
}
- 2.實(shí)現(xiàn)java.lang.Runnable接口
//實(shí)現(xiàn)runable
public class MyThread implements Runnable {
public void run() {
//...
}
}
public class MultiThread {
public static void main(String[] args) {
//Thread的構(gòu)造參數(shù)為 Runable接口
Thread t = new Thread(new MyThread());
t.start(); //啟動子線程
//主線程繼續(xù)同時(shí)向下執(zhí)行
//..
}
}
1.2 線程的結(jié)束
主線程執(zhí)行結(jié)束,子線程未結(jié)束,進(jìn)程不退出,為何? 如果把子線程設(shè)置為 deamon Thread
,則主線程退出,進(jìn)程就退出了.
所有非deamon線程都退出時(shí),進(jìn)程才退出.
1.3 暫停線程
java.lang.Thread.sleep(xxx)方法(注意是類方法)。使當(dāng)前正在執(zhí)行的線程暫停指定的時(shí)間,如果線程持有鎖,sleep方法并不會釋放鎖。
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
try {
Thread.sleep(1000); //當(dāng)前main線程暫停1000ms
} catch (InterruptedException e) {
}
}
}
}
上述代碼中,當(dāng)main線程調(diào)用Thread.sleep(1000)后,線程會被暫停,如果被interrupt,則會拋出InterruptedException異常。
1.4 線程中斷
java.lang.Thread#interrupt()方法
Thread實(shí)例方法。當(dāng)被interrupt的線程處于waitting狀態(tài)如wait、sleep、join時(shí),會拋出InterruptedException異常,異常拋出后線程內(nèi)的 中斷狀態(tài)被重置為false,所以捕獲異常后,要及時(shí)退出線程.。
事實(shí)上,interrupt方法只是改變目標(biāo)線程的中斷狀態(tài)(interrupt status),而那些會拋出InterruptedException異常的方法,如wait、sleep、join等,都是在方法內(nèi)部不斷地檢查中斷狀態(tài)的值
java.lang.Thread#isInterrupted()方法
Thread實(shí)例方法:用來檢查指定線程的中斷狀態(tài)。true為中斷狀態(tài); false為: 非中斷。
java.lang.Thread#interrupted方法
Thread類方法:返回調(diào)用此方法的線程的中斷狀態(tài),并清除中斷狀態(tài)(置為false) ;
1.5 線程同步
synchronized 是管程,細(xì)節(jié)查看管程資料
synchronized 可使用 對象實(shí)例鎖 和 類鎖.
package com.rock.multithread.base;
public class SynchronizedDemo {
/**
* 實(shí)例方法鎖 即 this 實(shí)例鎖
* @return
*/
public synchronized String 實(shí)例方法鎖(){
return "";
}
/**
* 同 this 實(shí)例鎖
* @return
*/
public String this鎖(){
synchronized (this){
return "";
}
}
/**
* 靜態(tài)方法鎖 即類鎖
* @return
*/
public static synchronized String 靜態(tài)方法鎖(){
return "";
}
/**
* 類鎖,同上
* @return
*/
public String 類方法鎖(){
synchronized (SynchronizedDemo.class){
return "";
}
}
}
1.6 線程協(xié)調(diào)
1Object#wait
放棄鎖,等通知
- 先進(jìn)入同步代碼塊,即拿到鎖
- xxx.wait(); 把當(dāng)前線程放入鎖對象的wait set 中,釋放鎖;
- 等待別的線程調(diào)用
通知方法
給信號后,剛執(zhí)行wait的線程 從wait set 中移除,放入鎖池隊(duì)列中,被系統(tǒng)調(diào)度喚醒后重新持有鎖,執(zhí)行wait后的代碼. -
通知方法
包括notify,notifyAll,interrupt
...
2.Object#notify / Object#notifyall
通知等待的線程
- 先進(jìn)入同步代碼塊,即拿到鎖
- notify / notifyall 給等待線程發(fā)信號,兩者的區(qū)別是,notify方法 只從 wait set中移動一個(gè)線程到鎖池中,notifyAll是 wait set中的全部線程都移到鎖池中.
- 繼續(xù)執(zhí)行notify/notifyAll 后的方法,直到退出同步代碼塊,才釋放鎖.這里是關(guān)鍵,發(fā)送通知后,線程不會立即終止執(zhí)行,所以后續(xù)的代碼可能還會修改競態(tài)條件.
4.wait的使用為何要加while
1.案例1
public class WaitNotifyDemo {
private volatile boolean isEmpty= false;
/**
* 假喚醒實(shí)例1 ,提供消費(fèi)條件,喚醒消費(fèi)線程,又把消費(fèi)條件給取消了,這樣消費(fèi)線程醒來,缺不滿足條件.
*/
public synchronized void fakeProvider1(){
System.out.println("冒牌provider 把isEmpty 設(shè)置為true,通知后,還把信號再修改為false");
isEmpty = true;
notify();//發(fā)送通知后,線程不會退出
isEmpty = false; //說不清的原因 信號又被重置了.這種情況下,consumer線程醒來后,其實(shí)isEmpty條件是不滿足的,所以應(yīng)該應(yīng)用用while循環(huán)來判斷條件
}
public synchronized void consumer1() throws InterruptedException {
//使用while循環(huán)來防止假喚醒.所謂的假喚醒本質(zhì)是,喚醒后不滿足繼續(xù)執(zhí)行的條件,所以繼續(xù)判斷下條件是否滿足,不滿足就繼續(xù)wait.
while (isEmpty){
//條件不滿足,等待
this.wait();
}
System.out.println("成功消費(fèi)一次,isEmpty 設(shè)置為true");
isEmpty = true;
}
}
2.案例2
public class WaitNotifyDemo2 {
private volatile boolean isEmpty= false;
/**
* 一個(gè)provider
* 多個(gè)線程執(zhí)行消費(fèi)
* 正常的設(shè)置消費(fèi)條件.發(fā)通知
*/
public synchronized void provider(){
System.out.println("provider 把isEmpty 設(shè)置為true,通知所有等待線程");
isEmpty = true;
notifyAll();
}
/**
* 多個(gè)消費(fèi)者都喚醒后,其中1個(gè)消費(fèi)者拿到鎖,執(zhí)行消費(fèi)后,把狀態(tài)重置了.另外一個(gè)消費(fèi)者的執(zhí)行條件就不滿足了,要繼續(xù)等.
* @throws InterruptedException
*/
public synchronized void consumer() throws InterruptedException {
//使用while循環(huán)來防止假喚醒.所謂的假喚醒本質(zhì)是,喚醒后不滿足繼續(xù)執(zhí)行的條件,所以繼續(xù)判斷下條件是否滿足,不滿足就繼續(xù)wait.
while (isEmpty){
//條件不滿足,等待
this.wait();
}
System.out.println("成功消費(fèi)一次,isEmpty 設(shè)置為true");
isEmpty = true;
}
}
1.6 線程讓步
java.lang.Thread.yield()方法:線程讓步 不會釋放鎖
線程執(zhí)行了yield()方法后,就會進(jìn)入Runnable(就緒狀態(tài)),【不同于sleep()和join()方法,因?yàn)檫@兩個(gè)方法是使線程進(jìn)入阻塞狀態(tài)】。除此之外,yield()方法還與線程優(yōu)先級有關(guān),當(dāng)某個(gè)線程調(diào)用yield()方法時(shí),就會從運(yùn)行狀態(tài)轉(zhuǎn)換到就緒狀態(tài)后,CPU從就緒狀態(tài)線程隊(duì)列中只會選擇與該線程優(yōu)先級相同或者更高優(yōu)先級的線程去執(zhí)行。
1.7 等待線程的結(jié)束
java.lang.Thread.join方法
實(shí)例方法.當(dāng)前線程調(diào)用目標(biāo)線程實(shí)例的join方法后,阻塞當(dāng)前線程直到目標(biāo)線程中run方法運(yùn)行結(jié)束.
1.8 通用線程狀態(tài)
這“五態(tài)模型”的詳細(xì)情況如下所示。
- 初始狀態(tài),指的是線程已經(jīng)被創(chuàng)建,但是還不允許分配CPU執(zhí)行。這個(gè)狀態(tài)屬于編程語言特有的,不過這里所謂的被創(chuàng)建,僅僅是在編程語言層面被創(chuàng)建,而在操作系統(tǒng)層面,真正的線程還沒有創(chuàng)建。
- 可運(yùn)行狀態(tài),指的是線程可以分配CPU執(zhí)行。在這種狀態(tài)下,真正的操作系統(tǒng)線程已經(jīng)被成功創(chuàng)建了,所以可以分配CPU執(zhí)行。
- 當(dāng)有空閑的CPU時(shí),操作系統(tǒng)會將其分配給一個(gè)處于可運(yùn)行狀態(tài)的線程,被分配到CPU的線程的狀態(tài)就轉(zhuǎn)換成了運(yùn)行狀態(tài)。
- 運(yùn)行狀態(tài)的線程如果調(diào)用一個(gè)阻塞的API(例如以阻塞方式讀文件)或者等待某個(gè)事件(例如條件變量),那么線程的狀態(tài)就會轉(zhuǎn)換到休眠狀態(tài),同時(shí)釋放CPU使用權(quán),休眠狀態(tài)的線程永遠(yuǎn)沒有機(jī)會獲得CPU使用權(quán)。當(dāng)?shù)却氖录霈F(xiàn)了,線程就會從休眠狀態(tài)轉(zhuǎn)換到可運(yùn)行狀態(tài)。
- 線程執(zhí)行完或者出現(xiàn)異常就會進(jìn)入終止?fàn)顟B(tài),終止?fàn)顟B(tài)的線程不會切換到其他任何狀態(tài),進(jìn)入終止?fàn)顟B(tài)也就意味著線程的生命周期結(jié)束了。
1.9 Java線程狀態(tài)
Java語言里則把可運(yùn)行狀態(tài)和運(yùn)行狀態(tài)合并了,這兩個(gè)狀態(tài)在操作系統(tǒng)調(diào)度層面有用,而JVM層面不關(guān)心這兩個(gè)狀態(tài),因?yàn)镴VM把線程調(diào)度交給操作系統(tǒng)處理了。
Java語言里把休眠狀態(tài) 細(xì)化為 BLOCKED(阻塞狀態(tài)),WAITING(無時(shí)限等待),TIMED_WAITING(有時(shí)限等待)
所以java線程狀態(tài)為:
NEW vs RUNNABLE
new出來對象后,調(diào)用其start方法.RUNNABLE vs BLOCKED
- 運(yùn)行中(runnable)的線程等待synchronized的隱式鎖 ,進(jìn)入blocked狀態(tài),等到獲得了synchronized的隱式鎖后,進(jìn)入runnable狀態(tài)
- 線程中執(zhí)行阻塞式API的調(diào)用后 還是處于runnable狀態(tài)
熟悉操作系統(tǒng)線程的生命周期的話,可能會有個(gè)疑問:線程調(diào)用阻塞式API時(shí),是否會轉(zhuǎn)換到BLOCKED狀態(tài)呢?在操作系統(tǒng)層面,線程是會轉(zhuǎn)換到休眠狀態(tài)的,但是在JVM層面,Java線程的狀態(tài)不會發(fā)生變化,也就是說Java線程的狀態(tài)會依然保持RUNNABLE狀態(tài)。JVM層面并不關(guān)心操作系統(tǒng)調(diào)度相關(guān)的狀態(tài),因?yàn)樵贘VM看來,等待CPU使用權(quán)(操作系統(tǒng)層面此時(shí)處于可執(zhí)行狀態(tài))與等待I/O(操作系統(tǒng)層面此時(shí)處于休眠狀態(tài))沒有區(qū)別,都是在等待某個(gè)資源,所以都?xì)w入了RUNNABLE狀態(tài)。
平時(shí)所謂的Java在調(diào)用阻塞式API時(shí),線程會阻塞,指的是操作系統(tǒng)線程的狀態(tài),并不是Java線程的狀態(tài)
- RUNNABLE vs WAITING
獲得synchronized隱式鎖的線程,調(diào)用無參數(shù)的Object.wait()方法。
調(diào)用無參數(shù)的Thread.join()方法。其中的join()是一種線程同步方法,thread-A中執(zhí)行thread-B.join();thread-A等待thread-B執(zhí)行完,其狀態(tài)會從RUNNABLE轉(zhuǎn)換到WAITING。當(dāng)線程thread-B執(zhí)行完,thread-A又會從WAITING狀態(tài)轉(zhuǎn)換到RUNNABLE。
調(diào)用LockSupport.park()方法。Java并發(fā)包中的鎖,都是基于LockSupport實(shí)現(xiàn)的。調(diào)用LockSupport.park()方法,當(dāng)前線程會阻塞,線程的狀態(tài)會從RUNNABLE轉(zhuǎn)換到WAITING。調(diào)用LockSupport.unpark(Thread thread)可喚醒目標(biāo)線程,目標(biāo)線程的狀態(tài)又會從WAITING狀態(tài)轉(zhuǎn)換到RUNNABLE。
- RUNNABLE vs TIMED_WAITING
- 調(diào)用帶超時(shí)參數(shù)的Thread.sleep(long millis)方法;
- 獲得synchronized隱式鎖的線程,調(diào)用帶超時(shí)參數(shù)的Object.wait(long timeout)方法;
- 調(diào)用帶超時(shí)參數(shù)的Thread.join(long millis)方法;
- 調(diào)用帶超時(shí)參數(shù)的LockSupport.parkNanos(Object blocker, long deadline)方法;
- 調(diào)用帶超時(shí)參數(shù)的LockSupport.parkUntil(long deadline)方法。
TIMED_WAITING和WAITING狀態(tài)的區(qū)別,僅僅是操作方法多了超時(shí)參數(shù)
- RUNNABLE vs TERMINATED
- 線程執(zhí)行完 run() 方法后,會自動轉(zhuǎn)換到TERMINATED狀態(tài)
- 如果執(zhí)行run()方法的時(shí)候異常拋出,也會導(dǎo)致線程終止
- Thread#stop 已經(jīng)標(biāo)記為@Deprecated,不建議使用了。正確的姿勢其實(shí)是調(diào)用interrupt()方法
- stop()方法會真的殺死線程,不給線程喘息的機(jī)會,如果線程持有synchronized隱式鎖,也不會釋放,那其他線程就再也沒機(jī)會獲得synchronized隱式鎖,這實(shí)在是太危險(xiǎn)了。所以該方法就不建議使用了,類似的方法還有suspend() 和 resume()方法,這兩個(gè)方法同樣也都不建議使用了,所以這里也就不多介紹了
- interrupt()方法僅僅是通知線程,線程有機(jī)會執(zhí)行一些后續(xù)操作,同時(shí)也可以無視這個(gè)通知。被interrupt的線程,是怎么收到通知的呢?一種是異常,另一種是主動檢測。
- 當(dāng)線程A處于WAITING、TIMED_WAITING狀態(tài)時(shí),如果其他線程調(diào)用線程A的interrupt()方法,會使線程A返回到RUNNABLE狀態(tài),同時(shí)線程A的代碼會觸發(fā)InterruptedException異常。上面我們提到轉(zhuǎn)換到WAITING、TIMED_WAITING狀態(tài)的觸發(fā)條件,都是調(diào)用了類似wait()、join()、sleep()這樣的方法,我們看這些方法的簽名,發(fā)現(xiàn)都會throws InterruptedException這個(gè)異常。這個(gè)異常的觸發(fā)條件就是:其他線程調(diào)用了該線程的interrupt()方法。
- 當(dāng)線程A處于RUNNABLE狀態(tài)時(shí),并且阻塞在java.nio.channels.InterruptibleChannel上時(shí),如果其他線程調(diào)用線程A的interrupt()方法,線程A會觸發(fā)java.nio.channels.ClosedByInterruptException這個(gè)異常;而阻塞在java.nio.channels.Selector上時(shí),如果其他線程調(diào)用線程A的interrupt()方法,線程A的java.nio.channels.Selector會立即返回。
- 上面這兩種情況屬于被中斷的線程通過異常的方式獲得了通知。還有一種是主動檢測,如果線程處于RUNNABLE狀態(tài),并且沒有阻塞在某個(gè)I/O操作上,例如中斷計(jì)算圓周率的線程A,這時(shí)就得依賴線程A主動檢測中斷狀態(tài)了。如果其他線程調(diào)用線程A的interrupt()方法,那么線程A可以通過isInterrupted()方法,檢測是不是自己被中斷了。
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略業(yè)務(wù)代碼無數(shù)
try {
Thread.sleep(100);
}catch (InterruptedException e){
//如果sleep方法拋出了此異常,表示被中斷了,此處應(yīng)該退出線程
//若catch中不退出線程,因則應(yīng)該應(yīng)該重置一下中斷標(biāo)示,因?yàn)閽伋霎惓:螅袛鄻?biāo)示會自動清除掉!
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
2.0 entry set vs wait set
參考管程之后這里就很清晰了.
鎖對象有個(gè) Entry Set 和 Wait Set 和當(dāng)前持鎖線程(The Owner)
WaitSet :處于wait狀態(tài)的線程,會被加入到wait set;
EntryList:處于等待鎖block狀態(tài)的線程,會被加入到entry set;
The Owner :表示某一線程成功競爭到對象鎖。
并發(fā)基本概念之 管程
Java多線程基礎(chǔ)(一)——線程與鎖
Java并發(fā)編程實(shí)戰(zhàn)
JVM源碼分析之Object.wait/notify實(shí)現(xiàn)