9.1.1 線程的概念
線程的概念來源於計算機的操作系統的進程的概念。進程是一個程序關於某個數據集的一次運行。也就是說,進程是運行中的程序,是程序的一次運行活動。
線程和進程的相似之處在於,線程和運行的程序都是單個順序控制流。有些教材將線程稱為輕量級進程(light weight process)。線程被看作是輕量級進程是因為它運行在一個程序的上下文內,並利用分配給程序的資源和環境。
作為單個順序控制流,線程必須在運行的程序中得到自己運行的資源,如必須有自己的執行棧和程序計數器。線程內運行的代碼只能在該上下文內。因此還有些教程將執行上下文(execution context)作為線程的同義詞。
所有的程序員都熟悉順序程序的編寫,如我們編寫的名稱排序和求素數的程序就是順序程序。順序程序都有開始、執行序列和結束,在程序執行的任何時刻,只有一個執行點。線程(thread)則是進程中的一個單個的順序控制流。單線程的概念很簡單,如圖9.1所示。
多線程(multi-thread)是指在單個的程序內可以同時運行多個不同的線程完成不同的任務,圖9.2說明了一個程序中同時有兩個線程運行。
圖9.1 單線程程序示意圖 圖9.2 多線程程序示意圖
有些程序中需要多個控制流並行執行。例如,
for(int i = 0; i < 100; i++)
System.out.println("Runner A = " + i);
for(int j = 0; j < 100; j++ )
System.out.println("Runner B = "+j);
上面的代碼段中,在只支持單線程的語言中,前一個循環不執行完不可能執行第二個循環。要使兩個循環同時執行,需要編寫多線程的程序。
很多應用程序是用多線程實現的,如Hot Java Web瀏覽器就是多線程應用的例子。在Hot Java 瀏覽器中,你可以一邊滾動屏幕,一邊下載Applet或圖像,可以同時播放動畫和聲音等。
9.1.2 Thread類和Runnable接口
多線程是一個程序中可以有多段代碼同時運行,那么這些代碼寫在哪里,如何創建線程對象呢?
首先,我們來看Java語言實現多線程編程的類和接口。在java.lang包中定義了Runnable接口和Thread類。
Runnable接口中只定義了一個方法,它的格式為:
- public abstract void run()
這個方法要由實現了Runnable接口的類實現。Runnable對象稱為可運行對象,一個線程的運行就是執行該對象的run()方法。
Thread類實現了Runnable接口,因此Thread對象也是可運行對象。同時Thread類也是線程類,該類的構造方法如下:
- public Thread()
- public Thread(Runnable target)
- public Thread(String name)
- public Thread(Runnable target, String name)
- public Thread(ThreadGroup group, Runnable target)
- public Thread(ThreadGroup group, String name)
- public Thread(ThreadGroup group, Runnable target, String name)
target為線程運行的目標對象,即線程調用start()方法啟動后運行那個對象的run()方法,該對象的類型為Runnable,若沒有指定目標對象,則以當前類對象為目標對象;name為線程名,group指定線程屬於哪個線程組(有關線程組的概念請參考9.6節)。
Thread類的常用方法有:
- public static Thread currentThread() 返回當前正在執行的線程對象的引用。
- public void setName(String name) 設置線程名。
- public String getName() 返回線程名。
- public static void sleep(long millis) throws InterruptedException
- public static void sleep(long millis, int nanos) throws InterruptedException
使當前正在執行的線程暫時停止執行指定的毫秒時間。指定時間過后,線程繼續執行。該方法拋出InterruptedException異常,必須捕獲。
- public void run() 線程的線程體。
- public void start() 由JVM調用線程的run()方法,啟動線程開始執行。
- public void setDaemon(boolean on) 設置線程為Daemon線程。
- public boolean isDaemon() 返回線程是否為Daemon線程。
- public static void yield() 使當前執行的線程暫停執行,允許其他線程執行。
- public ThreadGroup getThreadGroup() 返回該線程所屬的線程組對象。
- public void interrupt() 中斷當前線程。
- public boolean isAlive() 返回指定線程是否處於活動狀態。
9.2 線程的創建
本節介紹如何創建和運行線程的兩種方法。線程運行的代碼就是實現了Runnable接口的類的run()方法或者是Thread類的子類的run()方法,因此構造線程體就有兩種方法:
- 繼承Thread類並覆蓋它的run()方法;
- 實現Runnable接口並實現它的run()方法。
9.2.1 繼承Thread類創建線程
通過繼承Thread類,並覆蓋run()方法,這時就可以用該類的實例作為線程的目標對象。下面的程序定義了SimpleThread類,它繼承了Thread類並覆蓋了run()方法。
程序9.1 SimpleThread.java
public class SimpleThread extends Thread{
public SimpleThread(String str){
super(str);
}
public void run(){
for(int i=0; i<100; i++){
System.out.println(getName()+" = "+ i);
try{
sleep((int)(Math.random()*100));
}catch(InterruptedException e){}
}
System.out.println(getName()+ " DONE");
}
}
_____________________________________________________________________________▃
SimpleThread類繼承了Thread類,並覆蓋了run()方法,該方法就是線程體。
程序9.2 ThreadTest.java
public class ThreadTest{
public static void main(String args[]){
Thread t1 = new SimpleThread("Runner A");
Thread t2 = new SimpleThread("Runner B");
t1.start();
t2.start();
}
}
_____________________________________________________________________________▃
在ThreadTest類的main()方法中創建了兩個SimpleThread類的線程對象並調用線程類的start()方法啟動線程。構造線程時沒有指定目標對象,所以線程啟動后執行本類的run()方法。
注意,實際上ThreadTest程序中有三個線程同時運行。請試着將下段代碼加到main()方法中,分析程序運行結果。
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+"="+ i);
try{
Thread.sleep((int)(Math.random()*500));
}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+ " DONE");
}
從上述代碼執行結果可以看到,在應用程序的main()方法啟動時,JVM就創建一個主線程,在主線程中可以創建其他線程。
再看下面的程序:
程序9.3 MainThreadDemo.java
public class MainThreadDemo{
public static void main(String args[]){
Thread t = Thread.currentThread();
t.setName("MyThread");
System.out.println(t);
System.out.println(t.getName());
System.out.println(t.getThreadGroup().getName());
}
}
_____________________________________________________________________________▃
該程序輸出結果為:
Thread[MyThread, 5, main]
MyThread
main
上述程序在main()方法中聲明了一個Thread對象t,然后調用Thread類的靜態方法currentThread()獲得當前線程對象。然后重新設置該線程對象的名稱,最后輸出線程對象、線程組對象名和線程對象名。
9.2.2 實現Runnable接口創建線程
可以定義一個類實現Runnable接口,然后將該類對象作為線程的目標對象。實現Runnable接口就是實現run()方法。
下面程序通過實現Runnable接口構造線程體。
程序9.4 ThreadTest.java
class T1 implements Runnable{
public void run(){
for(int i=0;i<15;i++)
System.out.println("Runner A="+i);
}
}
class T2 implements Runnable{
public void run(){
for(int j=0;j<15;j++)
System.out.println("Runner B="+j);
}
}
public class ThreadTest{
public static void main(String args[]){
Thread t1=new Thread(new T1(),"Thread A");
Thread t2=new Thread(new T2(),"Thread B");
t1.start();
t2.start();
}
}
_____________________________________________________________________________▃
下面是一個小應用程序,利用線程對象在其中顯示當前時間。
程序9.5 ThreadTest.java
//<applet code="ClockDemo.class" height="200" width="300">
//</applet>
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.text.DateFormat;
public class ClockDemo extends JApplet{
private Thread clockThread = null;
private ClockPanel cp=new ClockPanel();
public void init(){
getContentPane().add(cp);
}
public void start() {
if (clockThread == null) {
clockThread = new Thread(cp, "Clock");
clockThread.start();
}
}
public void stop() {
clockThread = null;
}
}
class ClockPanel extends JPanel implements Runnable{
public void paintComponent(Graphics g) {
super.paintComponent(g);
Calendar cal = Calendar.getInstance();
Date date = cal.getTime();
DateFormat dateFormatter = DateFormat.getTimeInstance();
g.setColor(Color.BLUE);
g.setFont(new Font("TimesNewRoman",Font.BOLD,36));
g.drawString(dateFormatter.format(date), 50, 50);
}
public void run() {
while (true) {
repaint();
try {
Thread.sleep(1000);
} catch (InterruptedException e){ }
}
}
}
_____________________________________________________________________________▃
該小應用程序的運行結果如圖9.3所示:
圖9.3 ClockDemo的運行結果
9.3 線程的狀態與調度
9.3.1 線程的生命周期
線程從創建、運行到結束總是處於下面五個狀態之一:新建狀態、就緒狀態、運行狀態、阻塞狀態及死亡狀態。線程的狀態如圖9.4所示:
圖9.4 線程的五種狀態
下面以前面的Java小程序為例說明線程的狀態:
1. 新建狀態(New Thread)
當Applet啟動時調用Applet的start()方法,此時小應用程序就創建一個Thread對象clockThread。
public void start() {
if (clockThread == null) {
clockThread = new Thread(cp, "Clock");
clockThread.start();
}
}
當該語句執行后clockThread就處於新建狀態。處於該狀態的線程僅僅是空的線程對象,並沒有為其分配系統資源。當線程處於該狀態,你僅能啟動線程,調用任何其他方法是無意義的且會引發IllegalThreadStateException異常(實際上,當調用線程的狀態所不允許的任何方法時,運行時系統都會引發IllegalThreadStateException異常)。
注意cp作為線程構造方法的第一個參數,該參數必須是實現了Runnable接口的對象並提供線程運行的run()方法,第二個參數是線程名。
2. 就緒狀態(Runnable)
一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當線程對象調用start()方法即啟動了線程,如clockThread.start(); 語句就是啟動clockThread線程。start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回后,線程就處於就緒狀態。
處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他線程競爭CPU時間,只有獲得CPU時間才可以運行線程。因為在單CPU的計算機系統中,不可能同時運行多個線程,一個時刻僅有一個線程處於運行狀態。因此此時可能有多個線程處於就緒狀態。對多個處於就緒狀態的線程是由Java運行時系統的線程調度程序(thread scheduler)來調度的。
3. 運行狀態(Running)
當線程獲得CPU時間后,它才進入運行狀態,真正開始執行run()方法,這里run()方法中是一個循環,循環條件是true。
public void run() {
while (true) {
repaint();
try {
Thread.sleep(1000);
} catch (InterruptedException e){}
}
4. 阻塞狀態(Blocked)
線程運行過程中,可能由於各種原因進入阻塞狀態。所謂阻塞狀態是正在運行的線程沒有運行結束,暫時讓出CPU,這時其他處於就緒狀態的線程就可以獲得CPU時間,進入運行狀態。有關阻塞狀態在后面詳細討論。
5. 死亡狀態(Dead)
線程的正常結束,即run()方法返回,線程運行就結束了,此時線程就處於死亡狀態。本例子中,線程運行結束的條件是clockThread為null,而在小應用程序的stop()方法中,將clockThread賦值為null。即當用戶離開含有該小應用程序的頁面時,瀏覽器調用stop()方法,將clockThread賦值為null,這樣在run()的while循環時條件就為false,這樣線程運行就結束了。如果再重新訪問該頁面,小應用程序的start()方法又會重新被調用,重新創建並啟動一個新的線程。
public void stop() {
clockThread = null;
}
程序不能像終止小應用程序那樣通過調用一個方法來結束線程(小應用程序通過調用stop()方法結束小應用程序的運行)。線程必須通過run()方法的自然結束而結束。通常在run()方法中是一個循環,要么是循環結束,要么是循環的條件不滿足,這兩種情況都可以使線程正常結束,進入死亡狀態。
例如,下面一段代碼是一個循環:
public void run(){
int i = 0;
while(i<100){
i++;
System.out.println("i = " + i );
}
}
當該段代碼循環結束后,線程就自然結束了。注意一個處於死亡狀態的線程不能再調用該線程的任何方法。
9.3.2 線程的優先級和調度
Java的每個線程都有一個優先級,當有多個線程處於就緒狀態時,線程調度程序根據線程的優先級調度線程運行。
可以用下面方法設置和返回線程的優先級。
- public final void setPriority(int newPriority) 設置線程的優先級。
- public final int getPriority() 返回線程的優先級。
newPriority為線程的優先級,其取值為1到10之間的整數,也可以使用Thread類定義的常量來設置線程的優先級,這些常量分別為:Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY,它們分別對應於線程優先級的1、5和10,數值越大優先級越高。當創建Java線程時,如果沒有指定它的優先級,則它從創建該線程那里繼承優先級。
一般來說,只有在當前線程停止或由於某種原因被阻塞,較低優先級的線程才有機會運行。
前面說過多個線程可並發運行,然而實際上並不總是這樣。由於很多計算機都是單CPU的,所以一個時刻只能有一個線程運行,多個線程的並發運行只是幻覺。在單CPU機器上多個線程的執行是按照某種順序執行的,這稱為線程的調度(scheduling)。
大多數計算機僅有一個CPU,所以線程必須與其他線程共享CPU。多個線程在單個CPU是按照某種順序執行的。實際的調度策略隨系統的不同而不同,通常線程調度可以采用兩種策略調度處於就緒狀態的線程。
(1) 搶占式調度策略
Java運行時系統的線程調度算法是搶占式的 (preemptive)。Java運行時系統支持一種簡單的固定優先級的調度算法。如果一個優先級比其他任何處於可運行狀態的線程都高的線程進入就緒狀態,那么運行時系統就會選擇該線程運行。新的優先級較高的線程搶占(preempt)了其他線程。但是Java運行時系統並不搶占同優先級的線程。換句話說,Java運行時系統不是分時的(time-slice)。然而,基於Java Thread類的實現系統可能是支持分時的,因此編寫代碼時不要依賴分時。當系統中的處於就緒狀態的線程都具有相同優先級時,線程調度程序采用一種簡單的、非搶占式的輪轉的調度順序。
(2) 時間片輪轉調度策略
有些系統的線程調度采用時間片輪轉(round-robin)調度策略。這種調度策略是從所有處於就緒狀態的線程中選擇優先級最高的線程分配一定的CPU時間運行。該時間過后再選擇其他線程運行。只有當線程運行結束、放棄(yield)CPU或由於某種原因進入阻塞狀態,低優先級的線程才有機會執行。如果有兩個優先級相同的線程都在等待CPU,則調度程序以輪轉的方式選擇運行的線程。
9.4 線程狀態的改變
一個線程在其生命周期中可以從一種狀態改變到另一種狀態,線程狀態的變遷如圖9.5所示:
圖9.5 線程狀態的改變
9.4.1 控制線程的啟動和結束
當一個新建的線程調用它的start()方法后即進入就緒狀態,處於就緒狀態的線程被線程調度程序選中就可以獲得CPU時間,進入運行狀態,該線程就開始運行run()方法。
控制線程的結束稍微復雜一點。如果線程的run()方法是一個確定次數的循環,則循環結束后,線程運行就結束了,線程對象即進入死亡狀態。如果run()方法是一個不確定循環,早期的方法是調用線程對象的stop()方法,然而由於該方法可能導致線程死鎖,因此從1.1版開始,不推薦使用該方法結束線程。一般是通過設置一個標志變量,在程序中改變標志變量的值實現結束線程。請看下面的例子:
程序9.6 ThreadStop.java
import java.util.*;
class Timer implements Runnable{
boolean flag=true;
public void run(){
while(flag){
System.out.print("\r\t"+new Date()+"...");
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
}
System.out.println("\n"+Thread.currentThread().getName()+" Stop");
}
public void stopRun(){
flag = false;
}
}
public class ThreadStop{
public static void main(String args[]){
Timer timer = new Timer();
Thread thread = new Thread(timer);
thread.setName("Timer");
thread.start();
for(int i=0;i<100;i++){
System.out.print("\r"+i);
try{
Thread.sleep(100);
}catch(InterruptedException e){}
}
timer.stopRun();
}
}
_____________________________________________________________________________▃
該程序在Timer類中定義了一個布而變量flag,同時定義了一個stopRun()方法,在其中將該變量設置為false。在主程序中通過調用該方法,從而改變該變量的值,使得run()方法的while循環條件不滿足,從而實現結束線程的運行。
說明 在Thread類中除了stop()方法被標注為不推薦(deprecated) 使用外,suspend()方法和resume()方法也被標明不推薦使用,這兩個方法原來用作線程的掛起和恢復。
9.4.2 線程阻塞條件
處於運行狀態的線程除了可以進入死亡狀態外,還可能進入就緒狀態和阻塞狀態。下面分別討論這兩種情況:
(1) 運行狀態到就緒狀態
處於運行狀態的線程如果調用了yield()方法,那么它將放棄CPU時間,使當前正在運行的線程進入就緒狀態。這時有幾種可能的情況:如果沒有其他的線程處於就緒狀態等待運行,該線程會立即繼續運行;如果有等待的線程,此時線程回到就緒狀態狀態與其他線程競爭CPU時間,當有比該線程優先級高的線程時,高優先級的線程進入運行狀態,當沒有比該線程優先級高的線程時,但有同優先級的線程,則由線程調度程序來決定哪個線程進入運行狀態,因此線程調用yield()方法只能將CPU時間讓給具有同優先級的或高優先級的線程而不能讓給低優先級的線程。
一般來說,在調用線程的yield()方法可以使耗時的線程暫停執行一段時間,使其他線程有執行的機會。
(2) 運行狀態到阻塞狀態
有多種原因可使當前運行的線程進入阻塞狀態,進入阻塞狀態的線程當相應的事件結束或條件滿足時進入就緒狀態。使線程進入阻塞狀態可能有多種原因:
① 線程調用了sleep()方法,線程進入睡眠狀態,此時該線程停止執行一段時間。當時間到時該線程回到就緒狀態,與其他線程競爭CPU時間。
Thread類中定義了一個interrupt()方法。一個處於睡眠中的線程若調用了interrupt()方法,該線程立即結束睡眠進入就緒狀態。
② 如果一個線程的運行需要進行I/O操作,比如從鍵盤接收數據,這時程序可能需要等待用戶的輸入,這時如果該線程一直占用CPU,其他線程就得不到運行。這種情況稱為I/O阻塞。這時該線程就會離開運行狀態而進入阻塞狀態。Java語言的所有I/O方法都具有這種行為。
③ 有時要求當前線程的執行在另一個線程執行結束后再繼續執行,這時可以調用join()方法實現,join()方法有下面三種格式:
- public void join() throws InterruptedException 使當前線程暫停執行,等待調用該方法的線程結束后再執行當前線程。
- public void join(long millis) throws InterruptedException 最多等待millis毫秒后,當前線程繼續執行。
- public void join(long millis, int nanos) throws InterruptedException 可以指定多少毫秒、多少納秒后繼續執行當前線程。
上述方法使當前線程暫停執行,進入阻塞狀態,當調用線程結束或指定的時間過后,當前線程線程進入就緒狀態,例如執行下面代碼:
t.join();
將使當前線程進入阻塞狀態,當線程t執行結束后,當前線程才能繼續執行。
④ 線程調用了wait()方法,等待某個條件變量,此時該線程進入阻塞狀態。直到被通知(調用了notify()或notifyAll()方法)結束等待后,線程回到就緒狀態。
⑤ 另外如果線程不能獲得對象鎖,也進入就緒狀態。
后兩種情況在下一節討論。
9.5 線程的同步與共享
前面程序中的線程都是獨立的、異步執行的線程。但在很多情況下,多個線程需要共享數據資源,這就涉及到線程的同步與資源共享的問題。
9.5.1 資源沖突
下面的例子說明,多個線程共享資源,如果不加以控制可能會產生沖突。
程序9.7 CounterTest.java
class Num{
private int x=0;
private int y=0;
void increase(){
x++;
y++;
}
void testEqual(){
System.out.println(x+","+y+":"+(x==y));
}
}
class Counter extends Thread{
private Num num;
Counter(Num num){
this.num=num;
}
public void run(){
while(true){
num.increase();
}
}
}
public class CounterTest{
public static void main(String[] args){
Num num = new Num();
Thread count1 = new Counter(num);
Thread count2 = new Counter(num);
count1.start();
count2.start();
for(int i=0;i<100;i++){
num.testEqual();
try{
Thread.sleep(100);
}catch(InterruptedException e){ }
}
}
}
_____________________________________________________________________________▃
上述程序在CounterTest類的main()方法中創建了兩個線程類Counter的對象count1和count2,這兩個對象共享一個Num類的對象num。兩個線程對象開始運行后,都調用同一個對象num的increase()方法來增加num對象的x和y的值。在main()方法的for()循環中輸出num對象的x和y的值。程序輸出結果有些x、y的值相等,大部分x、y的值不相等。
出現上述情況的原因是:兩個線程對象同時操作一個num對象的同一段代碼,通常將這段代碼段稱為臨界區(critical sections)。在線程執行時,可能一個線程執行了x++語句而尚未執行y++語句時,系統調度另一個線程對象執行x++和y++,這時在主線程中調用testEqual()方法輸出x、y的值不相等。
這里可能出現x的值小於y的值的情況,為什么?
9.5.2 對象鎖的實現
上述程序的運行結果說明了多個線程訪問同一個對象出現了沖突,為了保證運行結果正確(x、y的值總相等),可以使用Java語言的synchronized關鍵字,用該關鍵字修飾方法。用synchronized關鍵字修飾的方法稱為同步方法,Java平台為每個具有synchronized代碼段的對象關聯一個對象鎖(object lock)。這樣任何線程在訪問對象的同步方法時,首先必須獲得對象鎖,然后才能進入synchronized方法,這時其他線程就不能再同時訪問該對象的同步方法了(包括其他的同步方法)。
通常有兩種方法實現對象鎖:
(1) 在方法的聲明中使用synchronized關鍵字,表明該方法為同步方法。
對於上面的程序我們可以在定義Num類的increase()和testEqual()方法時,在它們前面加上synchronized關鍵字,如下所示:
synchronized void increase(){
x++;
y++;
}
synchronized void testEqual(){
System.out.println(x+","+y+":"+(x==y)+":"+(x<y));
}
一個方法使用synchronized關鍵字修飾后,當一個線程調用該方法時,必須先獲得對象鎖,只有在獲得對象鎖以后才能進入synchronized方法。一個時刻對象鎖只能被一個線程持有。如果對象鎖正在被一個線程持有,其他線程就不能獲得該對象鎖,其他線程就必須等待持有該對象鎖的線程釋放鎖。
如果類的方法使用了synchronized關鍵字修飾,則稱該類對象是線程安全的,否則是線程不安全的。
如果只為increase()方法添加synchronized 關鍵字,結果還會出現x、y的值不相等的情況,請考慮為什么?
(2) 前面實現對象鎖是在方法前加上synchronized 關鍵字,這對於我們自己定義的類很容易實現,但如果使用類庫中的類或別人定義的類在調用一個沒有使用synchronized關鍵字修飾的方法時,又要獲得對象鎖,可以使用下面的格式:
synchronized(object){
//方法調用
}
假如Num類的increase()方法沒有使用synchronized 關鍵字,我們在定義Counter類的run()方法時可以按如下方法使用synchronized為部分代碼加鎖。
public void run(){
while(true){
synchronized (num){
num.increase();
}
}
}
同時在main()方法中調用testEqual()方法也用synchronized關鍵字修飾,這樣得到的結果相同。
synchronized(num){
num.testEqual();
}
對象鎖的獲得和釋放是由Java運行時系統自動完成的。
每個類也可以有類鎖。類鎖控制對類的synchronized static代碼的訪問。請看下面的例子:
public class X{
static int x, y;
static synchronized void foo(){
x++;
y++;
}
}
當foo()方法被調用時(如,使用X.foo()),調用線程必須獲得X類的類鎖。
9.5.3 線程間的同步控制
在多線程的程序中,除了要防止資源沖突外,有時還要保證線程的同步。下面通過生產者-消費者模型來說明線程的同步與資源共享的問題。
假設有一個生產者(Producer),一個消費者(Consumer)。生產者產生0~9的整數,將它們存儲在倉庫(CubbyHole)的對象中並打印出這些數來;消費者從倉庫中取出這些整數並將其也打印出來。同時要求生產者產生一個數字,消費者取得一個數字,這就涉及到兩個線程的同步問題。
這個問題就可以通過兩個線程實現生產者和消費者,它們共享CubbyHole一個對象。如果不加控制就得不到預期的結果。
1. 不同步的設計
首先我們設計用於存儲數據的類,該類的定義如下:
程序9.8 CubbyHole.java
class CubbyHole{
private int content ;
public synchronized void put(int value){
content = value;
}
public synchronized int get(){
return content ;
}
}
_____________________________________________________________________________▃
CubbyHole類使用一個私有成員變量content用來存放整數,put()方法和get()方法用來設置變量content的值。CubbyHole對象為共享資源,所以用synchronized關鍵字修飾。當put()方法或get()方法被調用時,線程即獲得了對象鎖,從而可以避免資源沖突。
這樣當Producer對象調用put()方法是,它鎖定了該對象,Consumer對象就不能調用get()方法。當put()方法返回時,Producer對象釋放了CubbyHole的鎖。類似地,當Consumer對象調用CubbyHole的get()方法時,它也鎖定該對象,防止Producer對象調用put()方法。
接下來我們看Producer和Consumer的定義,這兩個類的定義如下:
程序9.9 Producer.java
public class Producer extends Thread {
private CubbyHole cubbyhole;
private int number;
public Producer(CubbyHole c, int number) {
cubbyhole = c;
this.number = number;
}
public void run() {
for (int i = 0; i < 10; i++) {
cubbyhole.put(i);
System.out.println("Producer #" + this.number + " put: " + i);
try {
sleep((int)(Math.random() * 100));
} catch (InterruptedException e) { }
}
}
}
_____________________________________________________________________________▃
Producer類中定義了一個CubbyHole類型的成員變量cubbyhole,它用來存儲產生的整數,另一個成員變量number用來記錄線程號。這兩個變量通過構造方法傳遞得到。在該類的run()方法中,通過一個循環產生10個整數,每次產生一個整數,調用cubbyhole對象的put()方法將其存入該對象中,同時輸出該數。
下面是Consumer類的定義:
程序9.10 Consumer.java
public class Consumer extends Thread {
private CubbyHole cubbyhole;
private int number;
public Consumer(CubbyHole c, int number) {
cubbyhole = c;
this.number = number;
}
public void run() {
int value = 0;
for (int i = 0; i < 10; i++) {
value = cubbyhole.get();
System.out.println("Consumer #" + this.number + " got: " + value);
}
}
}
_____________________________________________________________________________▃
在Consumer類的run()方法中也是一個循環,每次調用cubbyhole的get()方法返回當前存儲的整數,然后輸出。
下面是主程序,在該程序的main()方法中創建一個CubbyHole對象c,一個Producer對象p1,一個Consumer對象c1,然后啟動兩個線程。
程序9.11 ProducerConsumerTest.java
public class ProducerConsumerTest {
public static void main(String[] args) {
CubbyHole c = new CubbyHole();
Producer p1 = new Producer(c, 1);
Consumer c1 = new Consumer(c, 1);
p1.start();
c1.start();
}
}
_____________________________________________________________________________▃
該程序中對CubbyHole類的設計,盡管使用了synchronized關鍵字實現了對象鎖,但這還不夠。程序運行可能出現下面兩種情況:
如果生產者的速度比消費者快,那么在消費者來不及取前一個數據之前,生產者又產生了新的數據,於是消費者很可能會跳過前一個數據,這樣就會產生下面的結果:
Consumer: 3
Producer: 4
Producer: 5
Consumer: 5
…
反之,如果消費者比生產者快,消費者可能兩次取同一個數據,可能產生下面的結果:
Producer: 4
Consumer: 4
Consumer: 4
Producer: 5
…
2. 監視器模型
為了避免上述情況發生,就必須使生產者線程向CubbyHole對象中存儲數據與消費者線程從CubbyHole對象中取得數據同步起來。為了達到這一目的,在程序中可以采用監視器(monitor)模型,同時通過調用對象的wait()方法和notify()方法實現同步。
下面是修改后的CubbyHole類的定義:
程序9.12 CubbyHole.java
class CubbyHole{
private int content ;
private boolean available=false;
public synchronized void put(int value){
while(available==true){
try{
wait();
}catch(InterruptedException e){}
}
content =value;
available=true;
notifyAll();
}
public synchronized int get(){
while(available==false){
try{
wait();
}catch(InterruptedException e){}
}
available=false;
notifyAll();
return content;
}
}
_____________________________________________________________________________▃
這里有一個boolean型的私有成員變量available用來指示內容是否可取。當available為true時表示數據已經產生還沒被取走,當available為false時表示數據已被取走還沒有存放新的數據。
當生產者線程進入put()方法時,首先檢查available的值,若其為false,才可執行put()方法,若其為true,說明數據還沒有被取走,該線程必須等待。因此在put()方法中調用CubbyHole對象的wait()方法等待。調用對象的wait()方法使線程進入等待狀態,同時釋放對象鎖。直到另一個線程對象調用了notify()或notifyAll()方法,該線程才可恢復運行。
類似地,當消費者線程進入get()方法時,也是先檢查available的值,若其為true,才可執行get()方法,若其為false,說明還沒有數據,該線程必須等待。因此在get()方法中調用CubbyHole對象的wait()方法等待。調用對象的wait()方法使線程進入等待狀態,同時釋放對象鎖。
上述過程就是監視器模型,其中CubbyHole對象為監視器。通過監視器模型可以保證生產者線程和消費者線程同步,結果正確。
程序的運行結果如下:
特別注意:wait()、notify()和notifyAll()方法是Object類定義的方法,並且這些方法只能用在synchronized代碼段中。它們的定義格式如下:
- public final void wait()
- public final void wait(long timeout)
- public final void wait(long timeout, int nanos)
當前線程必須具有對象監視器的鎖,當調用該方法時線程釋放監視器的鎖。調用這些方法使當前線程進入等待(阻塞)狀態,直到另一個線程調用了該對象的notify()方法或notifyAll()方法,該線程重新進入運行狀態,恢復執行。
timeout和nanos為等待的時間的毫秒和納秒,當時間到或其他對象調用了該對象的notify()方法或notifyAll()方法,該線程重新進入運行狀態,恢復執行。
wait()的聲明拋出了InterruptedException,因此程序中必須捕獲或聲明拋出該異常。
- public final void notify()
- public final void notifyAll()
喚醒處於等待該對象鎖的一個或所有的線程繼續執行,通常使用notifyAll()方法。
在生產者/消費者的例子中,CubbyHole類的put和get方法就是臨界區。當生產者修改它時,消費者不能問CubbyHole對象;當消費者取得值時,生產者也不能修改它。
9.6 線程組
所有Java線程都屬於某個線程組(thread group)。線程組提供了一個將多個線程組織成一個線程組對象來管理的機制,如可以通過一個方法調用來啟動線程組中的所有線程。
9.6.1 創建線程組
線程組是由java.lang包中的ThreadGroup類實現的。它的構造方法如下:
- public ThreadGroup(String name)
- public ThreadGroup(ThreadGroup parent, String name)
name為線程組名,parent為線程組的父線程組,若無該參數則新建線程組的父線程組為當前運行的線程的線程組。
當一個線程被創建時,運行時系統都將其放入一個線程組。創建線程時可以明確指定新建線程屬於哪個線程組,若沒有明確指定則放入缺省線程組中。一旦線程被指定屬於哪個線程組,便不能改變,不能刪除。
9.6.2 缺省線程組
如果在創建線程時沒有在構造方法中指定所屬線程組,運行時系統會自動將該線程放入創建該線程的線程所屬的線程組中。那么當我們創建線程時沒有指定線程組,它屬於哪個線程組呢?
當Java應用程序啟動時,Java運行時系統創建一個名main的ThreadGroup對象。除非另外指定,否則所有新建線程都屬於main線程組的成員。
在一個線程組內可以創建多個線程,也可以創建其它的線程組。一個程序中的線程組和線程構成一個樹型結構,如圖9.6所示:
圖9.6 線程組的樹型結構
如果在Applet中創建線程,新線程組可能不是main線程組,它依賴於使用的瀏覽器或Applet查看器。
創建屬於某個線程組的線程可以通過下面構造方法實現
- public Thread(ThreadGroup group, Runnable target)
- public Thread(ThreadGroup group, String name)
- public Thread(ThreadGroup group, Runnable target, String name)
如下面代碼創建的myThread線程屬於myThreadGroup線程組。
ThreadGroup myGroup = new ThreadGroup("My Group of Threads");
Thread myThread = new Thread(myGroup, "a thread for my group");
為了得到線程所屬的線程組可以調用Thread的getThreadGroup()方法,該方法返回ThreadGroup對象。可以通過下面方法獲得線程所屬線程組名。
myThread.getThreadGroup().getName()
一旦得到了線程組對象,就可查詢線程組的有關信息,如線程組中其他線程、也可僅通過調用一個方法就可實現修改線程組中的線程,如掛起、恢復或停止線程。
9.6.3 線程組操作方法
線程組類提供了有關方法可以對線程組操作。
- public final String getName() 返回線程組名。
- public final ThreadGroup getParent() 返回線程組的父線程組對象。
- public final void setMaxPriority(int pri) 設置線程組的最大優先級。線程組中的線程不能超過該優先級。
- public final int getMaxPriority() 返回線程組的最大優先級。
- public boolean isDestroyed() 測試該線程組對象是否已被銷毀。
- public int activeCount() 返回該線程組中活動線程的估計數。
- public int activeGroupCount() 返回該線程組中活動線程組的估計數。
- public final void destroy() 銷毀該線程組及其子線程組對象。當前線程組的所有線程必須已經停止。
9.7 小 結
Java語言內在支持多線程的程序設計。線程是進程中的一個單個的順序控制流,多線程是指單個程序內可以同時運行多個線程。
在Java程序中創建多線程的程序有兩種方法。一種是繼承Thread類並覆蓋其run()方法,另一種是實現Runnable接口並實現其run()方法。
線程從創建、運行到結束總是處於下面五個狀態之一:新建狀態、就緒狀態、運行狀態、阻塞狀態及死亡狀態。Java的每個線程都有一個優先級,當有多個線程處於就緒狀態時,線程調度程序根據線程的優先級調度線程運行。
線程都是獨立的、異步執行的線程,但在很多情況下,多個線程需要共享數據資源,這就涉及到線程的同步與資源共享的問題。
所有Java線程都屬於某個線程組。線程組提供了一個將多個線程組織成一個線程組對象來管理的機制,如可以通過一個方法調用來啟動線程組中的所有線程。