Java多線程--線程及相關(guān)的Java API
線程與進(jìn)程
進(jìn)程是線程的容器,程序是指令、數(shù)據(jù)的組織形式,進(jìn)程是程序的實(shí)體。
一個(gè)進(jìn)程中可以容納若干個(gè)線程,線程是輕量級(jí)的進(jìn)程,是程序執(zhí)行的最小單位。我們研究多線程而不是多進(jìn)程,因?yàn)榫€程之間的切換與調(diào)度的成本遠(yuǎn)小于進(jìn)程。
線程的生命周期
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WATING,
TERMINATED;
}
線程的所有狀態(tài)都在枚舉類State中定義了,其中:
NEW表示剛剛創(chuàng)建的線程,此時(shí)線程還沒有開始執(zhí)行。調(diào)用start()后線程進(jìn)入RUNNABLE狀態(tài),線程在執(zhí)行過程中遇到synchronized同步塊,就進(jìn)入BLOCKED阻塞狀態(tài),此時(shí)線程暫停執(zhí)行,直到獲得請(qǐng)求的鎖。WAITING和TIMED_WAITING都表示等待,區(qū)別是前者是無時(shí)間限制的等待,后者是有時(shí)限的等待。等待可以是執(zhí)行wait()方法后等待notify()方法將其喚醒,也可以是通過join()方法等待的線程等待目標(biāo)線程的執(zhí)行結(jié)束。一旦等待了期望事件,線程再次執(zhí)行,從等待狀態(tài)變成RUNNABLE狀態(tài)。線程執(zhí)行結(jié)束后,進(jìn)入TERMINATED狀態(tài)。
Java中的線程API
線程的新建
Thread t = new Thread();
t.start();
如上就創(chuàng)建了一個(gè)線程,要想這個(gè)線程創(chuàng)建后做點(diǎn)什么,需要覆寫Thread類中的run()方法。
Thread t = new Thread();
t.run();
不能使用上面的方式啟動(dòng)線程,這樣調(diào)用不會(huì)產(chǎn)生任何新的線程,只是一個(gè)單純的方法調(diào)用。要想開啟線程,不能使用run(),而應(yīng)該用start()。
還可以通過實(shí)現(xiàn)Runnable接口,覆寫接口中的run()方法,然后在Thread的構(gòu)造函數(shù)中傳入Runnale,此方法更為常用。
new Thread(new Runnale() {
@Override
public void run() {}
}).start();
由于run()是Runnable接口中的唯一方法,在Java8種可以簡單寫成:
new Thread(() -> {}).start();
線程的終止
Thread類中有個(gè)stop()方法,但是已經(jīng)被廢棄。該方法太過暴力,強(qiáng)行把執(zhí)行到一半的線程終止掉,可能會(huì)引起數(shù)據(jù)不一致的問題。stop()方法會(huì)直接終止線程,并立即釋放這個(gè)線程持有的鎖,而鎖恰好是用來維持對(duì)象的一致性的。
舉個(gè)例子User類中有id和name兩個(gè)字段,一開始id = 1, name = "ming", 我們要set數(shù)據(jù)id = 2, name = "jun"更新user,若剛寫入了id = 2,name還沒來得及寫入就stop()的話,立即釋放鎖,因此其他線程得到鎖讀取這個(gè)對(duì)象的話,就會(huì)得到id = 2, name = "ming",這不是我們想要的結(jié)果,數(shù)據(jù)已經(jīng)被寫壞了!
那么要如何終止線程呢?可以自定義退出線程的方法,比如設(shè)置標(biāo)志位
volatile boolean stoped;
public void myStop() {
stoped = true;
}
在線程合適的地方, 通過判斷stoped變量來手動(dòng)退出線程。
@Override
public void run() {
while (true) {
if (stoped) {
break;
}
}
// synchronized (object) {do sth.}
}
線程的中斷
線程中斷不會(huì)使線程立即退出,而是通知目標(biāo)線程“你應(yīng)該退出了”,退不退出是由目標(biāo)線程來決定的。
Thread類有個(gè)實(shí)例interrupt()方法,它通知目標(biāo)線程中斷,也就是設(shè)置中斷標(biāo)志位。
另一個(gè)實(shí)例方法isInterrupt()用于判斷當(dāng)前線程是否被中斷(通過檢查中斷標(biāo)志位);
靜態(tài)方法Thread.interrupted()也可以判斷當(dāng)前線程的中斷狀態(tài),但同時(shí)會(huì)清除當(dāng)前線程的中斷標(biāo)志位。
wait()和notify()
wait()是Object的方法,也就是說每個(gè)對(duì)象都能調(diào)用。在當(dāng)前線程中某個(gè)對(duì)象調(diào)用了wait()方法則該線程就停止執(zhí)行,轉(zhuǎn)為等待狀態(tài)。舉個(gè)例子,在線程T中,調(diào)用了obj.wait(),線程T就進(jìn)入了對(duì)象obj的等待隊(duì)列,則線程T就在對(duì)象obj上等待,一直等到其他線程調(diào)用了obj.notify(),這時(shí)obj從它的等待隊(duì)列中隨機(jī)喚醒一個(gè)線程(不一定是剛才的線程T)。所以notify()方法喚醒的選擇不是公平的,不是說先等待的線程就一定會(huì)先被喚醒。
Object類還有一個(gè)notifyAll()方法,會(huì)喚醒某對(duì)象等待隊(duì)列中的所有線程。
notify()和wait()方法在執(zhí)行前必須獲得對(duì)象的鎖。為此在synchronized (obj)中先獲得了鎖,才能調(diào)用wait()或者notify()方法,wait()會(huì)立刻釋放鎖,而notify()不會(huì)立刻釋放鎖,wait()狀態(tài)的線程也不能立刻獲得鎖;等到執(zhí)行notify()的線程退出同步塊后,才釋放鎖,此時(shí)其他處于wait()狀態(tài)的線程才能獲得該鎖。
線程的掛起和繼續(xù)執(zhí)行
Thread類中還有兩個(gè)已經(jīng)被廢棄的方法,分別是suspend()和resume()。
suspend()在導(dǎo)致線程暫停的同時(shí),不會(huì)釋放任何鎖,而且線程狀態(tài)還是Runnable,其他任何線程都得不到被該線程占用的鎖,直到在線程上執(zhí)行了resume(),被掛起的線程才能繼續(xù),其他阻塞在相關(guān)鎖上的線程也才可以繼續(xù)執(zhí)行。但是如如resume()不小心在suspend()之前調(diào)用了,可能造成線程被永久掛起,從而永久占用鎖。
注意和notify()、wait()方法的區(qū)別。由于方法已被廢棄,不再討論更多。
join()和yield()
join()表示將線程“加入”到當(dāng)前線程中,會(huì)一直阻塞當(dāng)前線程,直到該線程執(zhí)行完畢,或者說:當(dāng)前線程一直等待join入的線程執(zhí)行完畢后,才能繼續(xù)執(zhí)行。
public class Abc {
public static volatile int i;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {for (i = 0; i < 100; i++);});
t.start();
t.join();
System.out.println(i);
}
}
舉個(gè)例子,上面的代碼中有main線程和t線程,由于線程t的join,使得main線程中System.out.println(i)的一定是等待t線程執(zhí)行完畢后才能執(zhí)行,因此打印值一定是100.
join()的實(shí)現(xiàn)依賴于wait()。A線程中調(diào)用了B線程的join方法,則相當(dāng)于A線程調(diào)用了B線程的wait方法,在調(diào)用了B線程的wait方法后,A線程就會(huì)進(jìn)入阻塞狀態(tài)。所以A線程會(huì)等待B執(zhí)行完畢后才能繼續(xù)執(zhí)行。
yield()表示主動(dòng)禮讓當(dāng)前線程的CPU使用權(quán),讓出不是會(huì)不會(huì)回來參與CPU資源的爭奪,下次還能被分配到。調(diào)用該方法,好比是當(dāng)前線程說:我的工作暫時(shí)做完,給其他線程一些工作機(jī)會(huì)。
volatile關(guān)鍵字
volatile可以確保變量的可見性、有序性,不保證復(fù)雜操作的原子性。所以volatite對(duì)于原子性操作只能說有一定的幫助,但不能取代鎖。
當(dāng)用volatile修飾一個(gè)變量時(shí),相當(dāng)于告訴虛擬機(jī),這個(gè)變成極有可能被某些線程修改,確保該變量在被修改后,其他線程能“看到”該變量已經(jīng)被改變。
synchronized
上面說了volatile不能真正保證線程安全,只能保證一個(gè)線程修改了數(shù)據(jù)后,其他線程能看到這個(gè)改變。兩個(gè)線程同時(shí)修改同一個(gè)共享變量時(shí),仍然會(huì)產(chǎn)生沖突。
為了保證操作的原子性,可以使用同步塊,synchronized
被synchronized修飾的方法,或被其包起來的代碼段,任何時(shí)候只有一個(gè)線程可以執(zhí)行。這就避免了多個(gè)線程修改同一個(gè)數(shù)據(jù)的危險(xiǎn)。synchronized可以實(shí)現(xiàn)線程間的同步,主要是對(duì)被同步的代碼加鎖。
Dog dog = new Dog();
- 指定的加鎖對(duì)象,如
synchronized (dog) {}或者synchronized (Dog.class) {} - 作用于實(shí)例方法上,如
public synchronized void fun() {},此時(shí)使用的鎖是當(dāng)前實(shí)例。 - 作用于靜態(tài)方法,如
public static synchronized void fun() {},此時(shí)使用的鎖是當(dāng)前類。
注意:為了保證線程間同步,使得在多個(gè)線程間修改同一個(gè)數(shù)據(jù)時(shí),保證任何時(shí)候只有一個(gè)線程能修改。多個(gè)線程需要使用同一把鎖。
因?yàn)閟ynchronized中的代碼任何時(shí)候只能有一個(gè)線程在執(zhí)行它,所以可以說被synchronized限制的多個(gè)線程是串行執(zhí)行的。
線程的其他概念
線程組
ThreadGroup類可以容納多個(gè)線程,可以將多個(gè)功能相同的線程放入線程組中,便于管理。
守護(hù)線程(Daemon)
守護(hù)線程用于守護(hù)用戶進(jìn)程。守護(hù)進(jìn)程在后臺(tái)默默完成一些系統(tǒng)性的服務(wù),比如垃圾回收線程、JIT線程就可理解成守護(hù)線程。而用戶線程是用于完成程序的業(yè)務(wù)操作的,當(dāng)程序中的所有用戶線程執(zhí)行完畢,守護(hù)線程沒有了可守護(hù)的對(duì)象,程序自然就退出。即:在一個(gè)Java程序中只有守護(hù)線程時(shí),虛擬機(jī)就自然退出。
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
}
});
t.setDaemon(true);
t.start();
System.out.println("over");
}
在上面的代碼中,線程t被設(shè)置成守護(hù)線程(必須在start()之前調(diào)用),用戶線程main在打印"over"后執(zhí)行完畢,t沒有要守護(hù)的線程了,所以程序會(huì)出,如果不設(shè)置t為守護(hù)線程,我們知道由于while (true)程序永遠(yuǎn)不會(huì)退出。
線程的優(yōu)先級(jí)
Java的線程有優(yōu)先級(jí),高優(yōu)先級(jí)線程在競爭資源時(shí)更有優(yōu)勢,但這也是個(gè)概率問題,高優(yōu)先級(jí)也可能搶占失敗,低優(yōu)先級(jí)線程也不是說一定會(huì)饑餓。
Java中的線程有1~10的優(yōu)先級(jí),數(shù)字越大,優(yōu)先級(jí)越高。Thread類中內(nèi)置了三個(gè)默認(rèn)的優(yōu)先級(jí),如下。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
by @sunhaiyu
2018.4.13