什么是線程
從概念上來說,線程不難理解。指的是程序代碼的獨立執行路徑(it's an independent path of execution through program code)。當多線程執行的時候,一個線程執行的代碼通常與另一個線程執行的代碼不同。那么JVM是怎么管理每個線程的執行呢?JVM給每一個線程一個方法調用棧(method-call stack),除了跟蹤當前的二進制命令之外,還跟蹤局部變量和JVM傳過來的參數,以及方法的返回值。
Java通過java.lang.Thread
來完成多線程功能,每個Thread對象都對應一個線程執行體。線程執行的內容在Thread的run方法中,由于默認run方法是一個空方法,我們可以通過繼承Thread類,重寫run方法來實現我們的工作。
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();
}
}
}
當我們使用java ThreadDemo
去執行上述代碼時,JVM創建了一個主線程執行main方法,通過執行mt.start()之后,主線程通知JVM去創建另一個線程用來執行MyThread當中的run方法。當start()方法返回之后,主線程繼續執行for的代碼塊,而另一個線程執行run方法。
Thread類
為了能夠更熟練地使用Java多線程。我們需要了解構成Thread類的一些方法,他們包括如何開始一個線程,如何為線程命名,如何讓線程暫停,如何確定一個線程是否活躍,如果將一個線程連接至另一個線程,如何得到當前上下文活躍線程數量,以及如何做多線程的調試。
創建一個線程
線程有八個構造器,最簡單的是
Thread() //創建一個具有默認名字的線程實例
Thread(String name) //創建一個具有指定名字的線程實例
與之對應的還有
Thread(Runnable target)
Thread(Runnable target, String name)
上面兩組構造器,不同的只有Runnable參數,Runnable參數確定了那些Thread類之外的提供run函數的對象。最后四個構造器組合了上述構造器,同時添加了ThreadGroup參數。
其中構造器
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
可以讓你指定方法調用棧(method-call stack)的深度,這對于一些以遞歸方式實現的方法來說很有用(可以避免StackOverFlowErrors)。
Thread類和Thread類的子類,它們都不是具體的線程。它們描述了線程的屬性,例如線程的名字和線程執行的run方法。在調用一個Thread類的start方法時,JVM會按照Thread類描述的模板去創建一個線程并調用run方法。
在調試階段,區分一個線程和其他的線程是很有必要的。Java將線程的名字和線程相綁定。線程的名字默認為"Thread"+"-"+"從0開始的整型",例如"Thread-0"。我們在構造器里面可以傳入自定義的字符串作為名字。
線程的Sleep
調用Thread的靜態方法sleep(long millis)可以強制一個線程暫停millis毫秒。其他線程可以中斷正在休眠的線程,如果發生了,那么正在休眠的線程清醒然后拋出一個InterruptedException。因此sleep方法必須包含在try塊或者調用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語句塊來看看不調用sleep和調用sleep的差別。可以看出如果注釋掉try...catch塊后,PI的值打印出來為0,那是因為主線程執行的比計算PI的線程快,所以在PI計算完成之前已經將PI的值打印了出來。因此為了得到正確的PI值,我們要讓主線程休眠一會等待另一個線程完成PI的計算。
線程的死活
當程序調用了線程實例的start方法并在調用run方法前會有一段時間(用于線程的初始化)。當線程的run方法返回后并在JVM清理這個線程之前也有一段時間。在run方法調用前的一瞬間到run方法返回后的一瞬間之間的時間,Thread的isAlive方法會返回true,其余時間返回false。
當一個線程的運行依賴于另外一個線程的結果時,isAlive方法就變得很有用了。通過while循環不斷調用isAlive方法,直到返回false。這樣就可以確保一個線程在另一個線程結束后運行。
下面給出一個修改版本的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方法都很實用,所以Sun將它們打包成了三個方法:join()
、join(long millis)
和join(long millis, int nanos)
。當一個線程想要等待另一個線程結束時,這個線程可以通過另一個線程的引用來調用join方法。下面為PI計算代碼的Join方法實現。注意默認不帶參數的join方法是阻塞的,直到線程結束為止,而使用join(long millis)
可以設置一個超時時間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");
}
}
統計線程的情況
在某些場景下,我們可能希望知道當前程序里面有多少個活躍的線程。Thread類提供了一對方法用來幫助你完成這些工作:activeCount()
和enumerate(Thread[] threads)
。但是這些方法只能在當前線程所屬線程組的上下文里工作。換句話說,這些方法只能找到與當前線程屬于同一個線程組的活躍線程。
靜態方法activeCount()
返回當前線程組活躍的線程數量。一個程序使用activeCount()
的整型返回值線程引用數組的大小。為了獲得這些引用,程序必須調用靜態的enumerate(Thread[] threads)
方法,該方法的整型返回值線程引用數組的數量。
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]);
}
}
}
上述例子說明了這對方法的使用。輸出結果應該為Thread[main, 5, main]。第一個main代表線程的名字,5代表線程的優先級,第二個main代表該線程屬于哪個線程組。我們可能會覺得奇怪,為什么在輸出沒有看到系統的線程。Thread的靜態方法enumerate(Thread[] threads)
只能詢問與當前線程屬于同一個線程組的活躍線程。但是在ThreadGroup類里面包含了多個enumerate()
方法允許你去捕捉所有線程的引用。ThreadGroup將在之后的章節中提及。
不要依賴activeCount返回的結果:因為在activeCount方法到enumerate方法之間,有可能有線程會終止,這樣會導致activeCount返回值不在有效,用這個返回值去遍歷線程引用數組時會發生越界的錯誤。
線程調試
如果你的程序遇到故障,而你發現這個故障可能與一個線程有關,你可以通過Thread.dumpStack()
來取得這個線程的詳細信息。靜態的dumpStack()
方法提供了new Exception("Stack trace").printStackTrace()
的包裹。
線程的等級
并不是所有線程都是平等的。線程分為兩類,一種是用戶線程,一種是守護線程。一個用戶線程執行用戶程序的工作,這些工作必須在應用終止之前完成。而守護線程一般執行內務(例如垃圾回收器(garbage collection))和其他的后臺任務,這些后臺任務不需要依賴于應用的主要工作,但是應用的主要工作需要這些后臺任務。與用戶線程不同的是,守護線程不需要在用戶線程結束之前完成任務。當一個應用的開始線程(即用戶線程)終止時,JVM會檢查其他用戶線程是否還在運行,如果一些還在,那么JVM會阻止該應用的終止。但如果是守護線程的話,JVM會不管是否有后臺線程還在運行終止這個應用。如果想要得到當前線程的引用,可以使用Thread.currentThread()獲得。
當你調用線程對象的start()方法時,新創建的線程是用戶線程(默認)。如果想要創建一個守護線程,在調用start()方法前,需要調用setDeamon()方法來設置。我們可以使用Thread.isDaemon()來判斷一個線程是否為守護線程。
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);
}
}
上述程序根據是否傳入命令行參數來創建守護線程或用戶線程。如果是用戶線程,程序會一致運行,我們需要按下Ctrl+C來終止程序,如果是守護線程,守護進程會隨著主線程的終止而終止。
使用Runnable創建線程
除了通過擴展Thread類,重寫run方法來創建之外,還有別的方式創建線程。我們知道Java中不允許多繼承的存在,一個類如果繼承自非線程類,那么它就不能繼承自線程類。由于繼承的限制,我們如何把多線程引入到一個其他類的子類呢,Java提供了使用Runnable來創建線程的方法。使用Runnable實現計算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");
}
}