什么是線程
從概念上來說,線程不難理解合是。指的是程序代碼的獨(dú)立執(zhí)行路徑(it's an independent path of execution through program code)存崖。當(dāng)多線程執(zhí)行的時候,一個線程執(zhí)行的代碼通常與另一個線程執(zhí)行的代碼不同。那么JVM是怎么管理每個線程的執(zhí)行呢蜗帜?JVM給每一個線程一個方法調(diào)用棧(method-call stack),除了跟蹤當(dāng)前的二進(jìn)制命令之外资厉,還跟蹤局部變量和JVM傳過來的參數(shù)厅缺,以及方法的返回值。
Java通過java.lang.Thread
來完成多線程功能宴偿,每個Thread對象都對應(yīng)一個線程執(zhí)行體湘捎。線程執(zhí)行的內(nèi)容在Thread的run方法中,由于默認(rèn)run方法是一個空方法窄刘,我們可以通過繼承Thread類窥妇,重寫run方法來實(shí)現(xiàn)我們的工作。
Program 1: ThreadDemo.java
//ThreadDemo.java
class ThreadDemo
{
public static void main(String[] args)
{
MyThread mt = new MyThread();
mt.start();
for (int i = 0; i < 50; i++)
{
System.out.println("i = " + i + ", i * i = " + i * i);
}
}
}
class MyThread extends Thread
{
@Override
public void run()
{
for (int count = 1, row = 1; row < 20; count++, row++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}
當(dāng)我們使用java ThreadDemo
去執(zhí)行上述代碼時娩践,JVM創(chuàng)建了一個主線程執(zhí)行main方法活翩,通過執(zhí)行mt.start()之后烹骨,主線程通知JVM去創(chuàng)建另一個線程用來執(zhí)行MyThread當(dāng)中的run方法。當(dāng)start()方法返回之后材泄,主線程繼續(xù)執(zhí)行for的代碼塊沮焕,而另一個線程執(zhí)行run方法。
Thread類
為了能夠更熟練地使用Java多線程拉宗。我們需要了解構(gòu)成Thread類的一些方法峦树,他們包括如何開始一個線程,如何為線程命名旦事,如何讓線程暫停魁巩,如何確定一個線程是否活躍,如果將一個線程連接至另一個線程族檬,如何得到當(dāng)前上下文活躍線程數(shù)量歪赢,以及如何做多線程的調(diào)試。
創(chuàng)建一個線程
線程有八個構(gòu)造器单料,最簡單的是
Thread() //創(chuàng)建一個具有默認(rèn)名字的線程實(shí)例
Thread(String name) //創(chuàng)建一個具有指定名字的線程實(shí)例
與之對應(yīng)的還有
Thread(Runnable target)
Thread(Runnable target, String name)
上面兩組構(gòu)造器埋凯,不同的只有Runnable參數(shù),Runnable參數(shù)確定了那些Thread類之外的提供run函數(shù)的對象扫尖。最后四個構(gòu)造器組合了上述構(gòu)造器白对,同時添加了ThreadGroup參數(shù)。
其中構(gòu)造器
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
可以讓你指定方法調(diào)用棧(method-call stack)的深度换怖,這對于一些以遞歸方式實(shí)現(xiàn)的方法來說很有用(可以避免StackOverFlowErrors)甩恼。
Thread類和Thread類的子類,它們都不是具體的線程沉颂。它們描述了線程的屬性条摸,例如線程的名字和線程執(zhí)行的run方法。在調(diào)用一個Thread類的start方法時铸屉,JVM會按照Thread類描述的模板去創(chuàng)建一個線程并調(diào)用run方法钉蒲。
在調(diào)試階段,區(qū)分一個線程和其他的線程是很有必要的彻坛。Java將線程的名字和線程相綁定顷啼。線程的名字默認(rèn)為"Thread"+"-"+"從0開始的整型",例如"Thread-0"昌屉。我們在構(gòu)造器里面可以傳入自定義的字符串作為名字钙蒙。
線程的Sleep
調(diào)用Thread的靜態(tài)方法sleep(long millis)可以強(qiáng)制一個線程暫停millis毫秒。其他線程可以中斷正在休眠的線程间驮,如果發(fā)生了躬厌,那么正在休眠的線程清醒然后拋出一個InterruptedException。因此sleep方法必須包含在try塊或者調(diào)用sleep的方法拋出一個異常竞帽。
下面使用一個計算pi(圓周率)的程序來說明sleep的作用烤咧。
Program 2: CalcPI1.java
//CalcPI1.java
class CalcPI1
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
Thread.sleep (10); // Sleep for 10 milliseconds
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; // Initializes to 0.0, by default
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
可以注釋掉try...catch語句塊來看看不調(diào)用sleep和調(diào)用sleep的差別偏陪。可以看出如果注釋掉try...catch塊后煮嫌,PI的值打印出來為0,那是因?yàn)橹骶€程執(zhí)行的比計算PI的線程快抱虐,所以在PI計算完成之前已經(jīng)將PI的值打印了出來昌阿。因此為了得到正確的PI值,我們要讓主線程休眠一會等待另一個線程完成PI的計算恳邀。
線程的死活
當(dāng)程序調(diào)用了線程實(shí)例的start方法并在調(diào)用run方法前會有一段時間(用于線程的初始化)懦冰。當(dāng)線程的run方法返回后并在JVM清理這個線程之前也有一段時間。在run方法調(diào)用前的一瞬間到run方法返回后的一瞬間之間的時間谣沸,Thread的isAlive方法會返回true刷钢,其余時間返回false。
當(dāng)一個線程的運(yùn)行依賴于另外一個線程的結(jié)果時乳附,isAlive方法就變得很有用了内地。通過while循環(huán)不斷調(diào)用isAlive方法,直到返回false赋除。這樣就可以確保一個線程在另一個線程結(jié)束后運(yùn)行阱缓。
下面給出一個修改版本的PI計算代碼
Program 3: CalcPI2.java
//CalcPI2.java
class CalcPI2
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
while (mt.isAlive ())
try
{
Thread.sleep (10); // Sleep for 10 milliseconds
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; // Initializes to 0.0, by default
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
線程的Join
由于線程的sleep方法和isAlive方法都很實(shí)用,所以Sun將它們打包成了三個方法:join()
举农、join(long millis)
和join(long millis, int nanos)
荆针。當(dāng)一個線程想要等待另一個線程結(jié)束時,這個線程可以通過另一個線程的引用來調(diào)用join方法颁糟。下面為PI計算代碼的Join方法實(shí)現(xiàn)航背。注意默認(rèn)不帶參數(shù)的join方法是阻塞的,直到線程結(jié)束為止棱貌,而使用join(long millis)
可以設(shè)置一個超時時間millis玖媚。
Program 4: CalcPI3.java
// CalcPI3.java
class CalcPI3
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
mt.join ();
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; // Initializes to 0.0, by default
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
統(tǒng)計線程的情況
在某些場景下,我們可能希望知道當(dāng)前程序里面有多少個活躍的線程键畴。Thread類提供了一對方法用來幫助你完成這些工作:activeCount()
和enumerate(Thread[] threads)
最盅。但是這些方法只能在當(dāng)前線程所屬線程組的上下文里工作。換句話說起惕,這些方法只能找到與當(dāng)前線程屬于同一個線程組的活躍線程涡贱。
靜態(tài)方法activeCount()
返回當(dāng)前線程組活躍的線程數(shù)量。一個程序使用activeCount()
的整型返回值線程引用數(shù)組的大小惹想。為了獲得這些引用问词,程序必須調(diào)用靜態(tài)的enumerate(Thread[] threads)
方法,該方法的整型返回值線程引用數(shù)組的數(shù)量嘀粱。
Program 5: ThreadCensus.java
//ThreadCensus.java
public class ThreadCensus
{
public static void main(String[] args)
{
Thread[] threads = new Thread[Thread.activeCount()];
int n = Thread.enumerate(threads);
for (int i = 0; i < n; i++)
{
System.out.println(threads[i]);
}
}
}
上述例子說明了這對方法的使用激挪。輸出結(jié)果應(yīng)該為Thread[main, 5, main]辰狡。第一個main代表線程的名字,5代表線程的優(yōu)先級垄分,第二個main代表該線程屬于哪個線程組宛篇。我們可能會覺得奇怪,為什么在輸出沒有看到系統(tǒng)的線程薄湿。Thread的靜態(tài)方法enumerate(Thread[] threads)
只能詢問與當(dāng)前線程屬于同一個線程組的活躍線程叫倍。但是在ThreadGroup類里面包含了多個enumerate()
方法允許你去捕捉所有線程的引用。ThreadGroup將在之后的章節(jié)中提及豺瘤。
不要依賴activeCount返回的結(jié)果:因?yàn)樵赼ctiveCount方法到enumerate方法之間吆倦,有可能有線程會終止,這樣會導(dǎo)致activeCount返回值不在有效坐求,用這個返回值去遍歷線程引用數(shù)組時會發(fā)生越界的錯誤蚕泽。
線程調(diào)試
如果你的程序遇到故障,而你發(fā)現(xiàn)這個故障可能與一個線程有關(guān)桥嗤,你可以通過Thread.dumpStack()
來取得這個線程的詳細(xì)信息须妻。靜態(tài)的dumpStack()
方法提供了new Exception("Stack trace").printStackTrace()
的包裹。
線程的等級
并不是所有線程都是平等的砸逊。線程分為兩類璧南,一種是用戶線程,一種是守護(hù)線程师逸。一個用戶線程執(zhí)行用戶程序的工作司倚,這些工作必須在應(yīng)用終止之前完成。而守護(hù)線程一般執(zhí)行內(nèi)務(wù)(例如垃圾回收器(garbage collection))和其他的后臺任務(wù)篓像,這些后臺任務(wù)不需要依賴于應(yīng)用的主要工作动知,但是應(yīng)用的主要工作需要這些后臺任務(wù)。與用戶線程不同的是员辩,守護(hù)線程不需要在用戶線程結(jié)束之前完成任務(wù)盒粮。當(dāng)一個應(yīng)用的開始線程(即用戶線程)終止時,JVM會檢查其他用戶線程是否還在運(yùn)行奠滑,如果一些還在丹皱,那么JVM會阻止該應(yīng)用的終止。但如果是守護(hù)線程的話宋税,JVM會不管是否有后臺線程還在運(yùn)行終止這個應(yīng)用摊崭。如果想要得到當(dāng)前線程的引用,可以使用Thread.currentThread()獲得杰赛。
當(dāng)你調(diào)用線程對象的start()方法時呢簸,新創(chuàng)建的線程是用戶線程(默認(rèn))。如果想要創(chuàng)建一個守護(hù)線程,在調(diào)用start()方法前根时,需要調(diào)用setDeamon()方法來設(shè)置瘦赫。我們可以使用Thread.isDaemon()來判斷一個線程是否為守護(hù)線程。
Program 6: UserDaemonThreadDemo.java
//UserDaemonThreadDemo.java
public class UserDaemonThreadDemo
{
public static void main(String[] args)
{
if (args.length == 0)
{
new MyThread().start();
}
else
{
MyThread mt = new MyThread();
mt.setDaemon(true);
mt.start();
}
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
}
}
}
class MyThread extends Thread
{
@Override
public void run()
{
System.out.println("Daemon:" + isDaemon());
while(true);
}
}
上述程序根據(jù)是否傳入命令行參數(shù)來創(chuàng)建守護(hù)線程或用戶線程蛤迎。如果是用戶線程确虱,程序會一致運(yùn)行,我們需要按下Ctrl+C來終止程序忘苛,如果是守護(hù)線程蝉娜,守護(hù)進(jìn)程會隨著主線程的終止而終止。
使用Runnable創(chuàng)建線程
除了通過擴(kuò)展Thread類扎唾,重寫run方法來創(chuàng)建之外,還有別的方式創(chuàng)建線程南缓。我們知道Java中不允許多繼承的存在胸遇,一個類如果繼承自非線程類,那么它就不能繼承自線程類汉形。由于繼承的限制纸镊,我們?nèi)绾伟讯嗑€程引入到一個其他類的子類呢,Java提供了使用Runnable來創(chuàng)建線程的方法概疆。使用Runnable實(shí)現(xiàn)計算PI的線程逗威。
Program 7: CalcPI4.java
//CalcPI4.java
class CalcPI4
{
public static void main (String [] args)
{
MyThread runnable = new MyThread();
Thread mt = new Thread(runnable);
mt.start ();
try
{
Thread.sleep (10); // Sleep for 10 milliseconds
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + runnable.pi);
}
}
class MyThread implements Runnable
{
boolean negative = true;
double pi; // Initializes to 0.0, by default
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}