Java多線程初學者指南(1):線程簡介
出處:http://m.tkk7.com/nokiaguy/archive/2009/03/archive/2009/03/archive/2009/03/archive/2009/03/14/259733.html
一、線程概述
線程是程序運行的基本執行單元。當操作系統(不包括單線程的操作系統,如微軟早期的DOS)在執行一個程序時,會在系統中建立一個進程,而在這個進程中,必須至少建立一個線程(這個線程被稱為主線程)來作為這個程序運行的入口點。因此,在操作系統中運行的任何程序都至少有一個主線程。
進程和線程是現代操作系統中兩個必不可少的運行模型。在操作系統中可以有多個進程,這些進程包括系統進程(由操作系統內部建立的進程)和用戶進程(由用戶程序建立的進程);一個進程中可以有一個或多個線程。進程和進程之間不共享內存,也就是說系統中的進程是在各自獨立的內存空間中運行的。而一個進程中的線可以共享系統分派給這個進程的內存空間。
線程不僅可以共享進程的內存,而且還擁有一個屬于自己的內存空間,這段內存空間也叫做線程棧, 是在建立線程時由系統分配的,主要用來保存線程內部所使用的數據,如線程執行函數中所定義的變量。
注意:任何一個線程在建立時都會執行一個函數,這個函數叫做線程執行函數。也可以將這個函數看做線程的入口點(類似于程序中的main函數)。無論使用什么語言或技術來建立線程,都必須執行這個函數(這個函數的表現形式可能不一樣,但都會有一個這樣的函數)。如在Windows中用于建立線程的API函數CreateThread的第三個參數就是這個執行函數的指針。
在操作系統將進程分成多個線程后,這些線程可以在操作系統的管理下并發執行,從而大大提高了程序的運行效率。雖然線程的執行從宏觀上看是多個線程同時執行,但實際上這只是操作系統的障眼法。由于一塊CPU同時只能執行一條指令,因此,在擁有一塊CPU的計算機上不可能同時執行兩個任務。而操作系統為了能提高程序的運行效率,在一個線程空閑時會撤下這個線程,并且會讓其他的線程來執行,這種方式叫做線程調度。我們之所以從表面上看是多個線程同時執行,是因為不同線程之間切換的時間非常短,而且在一般情況下切換非常頻繁。假設我們有線程A和B。在運行時,可能是A執行了1毫秒后,切換到B后,B又執行了1毫秒,然后又切換到了A,A又執行1毫秒。由于1毫秒的時間對于普通人來說是很難感知的,因此,從表面看上去就象A和B同時執行一樣,但實際上A和B是交替執行的。
二、線程給我們帶來的好處
如果能合理地使用線程,將會減少開發和維護成本,甚至可以改善復雜應用程序的性能。如在GUI應用程序中,還以通過線程的異步特性來更好地處理事件;在應用服務器程序中可以通過建立多個線程來處理客戶端的請求。線程甚至還可以簡化虛擬機的實現,如Java虛擬機(JVM)的垃圾回收器(garbage collector)通常運行在一個或多個線程中。因此,使用線程將會從以下五個方面來改善我們的應用程序:
1. 充分利用CPU資源
現在世界上大多數計算機只有一塊CPU。因此,充分利用CPU資源顯得尤為重要。當執行單線程程序時,由于在程序發生阻塞時CPU可能會處于空閑狀態。這將造成大量的計算資源的浪費。而在程序中使用多線程可以在某一個線程處于休眠或阻塞時,而CPU又恰好處于空閑狀態時來運行其他的線程。這樣CPU就很難有空閑的時候。因此,CPU資源就得到了充分地利用。
2. 簡化編程模型
如果程序只完成一項任務,那只要寫一個單線程的程序,并且按著執行這個任務的步驟編寫代碼即可。但要完成多項任務,如果還使用單線程的話,那就得在在程序中判斷每項任務是否應該執行以及什么時候執行。如顯示一個時鐘的時、分、秒三個指針。使用單線程就得在循環中逐一判斷這三個指針的轉動時間和角度。如果使用三個線程分另來處理這三個指針的顯示,那么對于每個線程來說就是指行一個單獨的任務。這樣有助于開發人員對程序的理解和維護。
3. 簡化異步事件的處理
當一個服務器應用程序在接收不同的客戶端連接時最簡單地處理方法就是為每一個客戶端連接建立一個線程。然后監聽線程仍然負責監聽來自客戶端的請求。如果這種應用程序采用單線程來處理,當監聽線程接收到一個客戶端請求后,開始讀取客戶端發來的數據,在讀完數據后,read方法處于阻塞狀態,也就是說,這個線程將無法再監聽客戶端請求了。而要想在單線程中處理多個客戶端請求,就必須使用非阻塞的Socket連接和異步I/O。但使用異步I/O方式比使用同步I/O更難以控制,也更容易出錯。因此,使用多線程和同步I/O可以更容易地處理類似于多請求的異步事件。
4. 使GUI更有效率
使用單線程來處理GUI事件時,必須使用循環來對隨時可能發生的GUI事件進行掃描,在循環內部除了掃描GUI事件外,還得來執行其他的程序代碼。如果這些代碼太長,那么GUI事件就會被“凍結”,直到這些代碼被執行完為止。
在現代的GUI框架(如SWING、AWT和SWT)中都使用了一個單獨的事件分派線程(event dispatch thread,EDT)來對GUI事件進行掃描。當我們按下一個按鈕時,按鈕的單擊事件函數會在這個事件分派線程中被調用。由于EDT的任務只是對GUI事件進行掃描,因此,這種方式對事件的反映是非常快的。
5. 節約成本
提高程序的執行效率一般有三種方法:
(1)增加計算機的CPU個數。
(2)為一個程序啟動多個進程
(3)在程序中使用多進程。
第一種方法是最容易做到的,但同時也是最昂貴的。這種方法不需要修改程序,從理論上說,任何程序都可以使用這種方法來提高執行效率。第二種方法雖然不用購買新的硬件,但這種方式不容易共享數據,如果這個程序要完成的任務需要必須要共享數據的話,這種方式就不太方便,而且啟動多個線程會消耗大量的系統資源。第三種方法恰好彌補了第一種方法的缺點,而又繼承了它們的優點。也就是說,既不需要購買CPU,也不會因為啟太多的線程而占用大量的系統資源(在默認情況下,一個線程所占的內存空間要遠比一個進程所占的內存空間小得多),并且多線程可以模擬多塊CPU的運行方式,因此,使用多線程是提高程序執行效率的最廉價的方式。
三、Java的線程模型
由于Java是純面向對象語言,因此,Java的線程模型也是面向對象的。Java通過Thread類將線程所必須的功能都封裝了起來。要想建立一個線程,必須要有一個線程執行函數,這個線程執行函數對應Thread類的run方法。Thread類還有一個start方法,這個方法負責建立線程,相當于調用Windows的建立線程函數CreateThread。當調用start方法后,如果線程建立成功,并自動調用Thread類的run方法。因此,任何繼承Thread的Java類都可以通過Thread類的start方法來建立線程。如果想運行自己的線程執行函數,那就要覆蓋Thread類的run方法。
在Java的線程模型中除了Thread類,還有一個標識某個Java類是否可作為線程類的接口Runnable,這個接口只有一個抽象方法run,也就是Java線程模型的線程執行函數。因此,一個線程類的唯一標準就是這個類是否實現了Runnable接口的run方法,也就是說,擁有線程執行函數的類就是線程類。
從上面可以看出,在Java中建立線程有兩種方法,一種是繼承Thread類,另一種是實現Runnable接口,并通過Thread和實現Runnable的類來建立線程,其實這兩種方法從本質上說是一種方法,即都是通過Thread類來建立線程,并運行run方法的。但它們的大區別是通過繼承Thread類來建立線程,雖然在實現起來更容易,但由于Java不支持多繼承,因此,這個線程類如果繼承了Thread,就不能再繼承其他的類了,因此,Java線程模型提供了通過實現Runnable接口的方法來建立線程,這樣線程類可以在必要的時候繼承和業務有關的類,而不是Thread類。
Java多線程初學者指南(2):用Thread類創建線程
在Java中創建線程有兩種方法:使用Thread類和使用Runnable接口。在使用Runnable接口時需要建立一個Thread實例。因此,無論是通過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);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Runnable target
實現了Runnable接口的類的實例。要注意的是Thread類也實現了Runnable接口,因此,從Thread類繼承的類的實例也可以作為target傳入這個構造方法。
String name
線程的名子。這個名子可以在建立Thread實例后通過Thread類的setName方法設置。如果不設置線程的名子,線程就使用默認的線程名:Thread-N,N是線程建立的順序,是一個不重復的正整數。
ThreadGroup group
當前建立的線程所屬的線程組。如果不指定線程組,所有的線程都被加到一個默認的線程組中。關于線程組的細節將在后面的章節詳細討論。
long stackSize
線程棧的大小,這個值一般是CPU頁面的整數倍。如x86的頁面大小是4KB。在x86平臺下,默認的線程棧大小是12KB。
一個普通的Java類只要從Thread類繼承,就可以成為一個線程類。并可通過Thread類的start方法來執行線程代碼。雖然Thread類的子類可以直接實例化,但在子類中必須要覆蓋Thread類的run方法才能真正運行線程的代碼。下面的代碼給出了一個使用Thread類建立線程的例子:
001 package mythread;
002
003 public class Thread1 extends Thread
004 {
005 public void run()
006 {
007 System.out.println(this.getName());
008 }
009 public static void main(String[] args)
010 {
011 System.out.println(Thread.currentThread().getName());
012 Thread1 thread1 = new Thread1();
013 Thread1 thread2 = new Thread1 ();
014 thread1.start();
015 thread2.start();
016 }
017 }
上面的代碼建立了兩個線程:thread1和thread2。上述代碼中的005至008行是Thread1類的run方法。當在014和015行調用start方法時,系統會自動調用run方法。在007行使用this.getName()輸出了當前線程的名字,由于在建立線程時并未指定線程名,因此,所輸出的線程名是系統的默認值,也就是Thread-n的形式。在011行輸出了主線程的線程名。
上面代碼的運行結果如下:
main
Thread-0
Thread-1
從上面的輸出結果可以看出,第一行輸出的main是主線程的名子。后面的Thread-1和Thread-2分別是thread1和thread2的輸出結果。
注意:任何一個Java程序都必須有一個主線程。一般這個主線程的名子為main。只有在程序中建立另外的線程,才能算是真正的多線程程序。也就是說,多線程程序必須擁有一個以上的線程。
Thread類有一個重載構造方法可以設置線程名。除了使用構造方法在建立線程時設置線程名,還可以使用Thread類的setName方法修改線程名。要想通過Thread類的構造方法來設置線程名,必須在Thread的子類中使用Thread類的public Thread(String name)構造方法,因此,必須在Thread的子類中也添加一個用于傳入線程名的構造方法。下面的代碼給出了一個設置線程名的例子:
001 package mythread;
002
003 public class Thread2 extends Thread
004 {
005 private String who;
006
007 public void run()
008 {
009 System.out.println(who + ":" + this.getName());
010 }
011 public Thread2(String who)
012 {
013 super();
014 this.who = who;
015 }
016 public Thread2(String who, String name)
017 {
018 super(name);
019 this.who = who;
020 }
021 public static void main(String[] args)
022 {
023 Thread2 thread1 = new Thread2 ("thread1", "MyThread1");
024 Thread2 thread2 = new Thread2 ("thread2");
025 Thread2 thread3 = new Thread2 ("thread3");
026 thread2.setName("MyThread2");
027 thread1.start();
028 thread2.start();
029 thread3.start();
030 }
031
在類中有兩個構造方法:
第011行:public sample2_2(String who)
這個構造方法有一個參數:who。這個參數用來標識當前建立的線程。在這個構造方法中仍然調用Thread的默認構造方法public Thread( )。
第016行:public sample2_2(String who, String name)
這個構造方法中的who和第一個構造方法的who的含義一樣,而name參數就是線程的名名。在這個構造方法中調用了Thread類的public Thread(String name)構造方法,也就是第018行的super(name)。
在main方法中建立了三個線程:thread1、thread2和thread3。其中thread1通過構造方法來設置線程名,thread2通過setName方法來修改線程名,thread3未設置線程名。
運行結果如下:
thread1:MyThread1
thread2:MyThread2
thread3:Thread-1
從上面的輸出結果可以看出,thread1和thread2的線程名都已經修改了,而thread3的線程名仍然為默認值:Thread-1。thread3的線程名之所以不是Thread-2,而是Thread-1,這是因為在026行已經指定了thread2的Name,因此,啟動thread3時就將thread3的線程名設為Thread-1。因此就會得到上面的輸出結果。
注意:在調用start方法前后都可以使用setName設置線程名,但在調用start方法后使用setName修改線程名,會產生不確定性,也就是說可能在run方法執行完后才會執行setName。如果在run方法中要使用線程名,就會出現雖然調用了setName方法,但線程名卻未修改的現象。
Thread類的start方法不能多次調用,如不能調用兩次thread1.start()方法。否則會拋出一個IllegalThreadStateException異常。
實現Runnable
接口的類必須使用Thread
類的實例才能創建線程。通過Runnable
接口創建線程分為兩步:
1. 將實現Runnable接口的類實例化。
2. 建立一個Thread對象,并將第一步實例化后的對象作為參數傳入Thread類的構造方法。
最后通過Thread類的start方法建立線程。
下面的代碼演示了如何使用Runnable接口來創建線程:
package mythread;
public class MyRunnable implements Runnable
{
public void run()
{
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args)
{
MyRunnable t1 = new MyRunnable();
MyRunnable t2 = new MyRunnable();
Thread thread1 = new Thread(t1, "MyThread1");
Thread thread2 = new Thread(t2);
thread2.setName("MyThread2");
thread1.start();
thread2.start();
}
}
上面代碼的運行結果如下:
MyThread1
MyThread2
Java多線程初學者指南(4):線程的生命周期
與人有生老病死一樣,線程也同樣要經歷開始(等待)、運行、掛起和停止四種不同的狀態。這四種狀態都可以通過Thread
類中的方法進行控制。下面給出了Thread
類中和這四種狀態相關的方法。
// 開始線程
public void start( );
public void run( );
// 掛起和喚醒線程
public void resume( ); // 不建議使用
public void suspend( ); // 不建議使用
public static void sleep(long millis);
public static void sleep(long millis, int nanos);
// 終止線程
public void stop( ); // 不建議使用
public void interrupt( );
// 得到線程狀態
public boolean isAlive( );
public boolean isInterrupted( );
public static boolean interrupted( );
// join方法
public void join( ) throws InterruptedException;
一、創建并運行線程
線程在建立后并不馬上執行run方法中的代碼,而是處于等待狀態。線程處于等待狀態時,可以通過Thread類的方法來設置線程不各種屬性,如線程的優先級(setPriority)、線程名(setName)和線程的類型(setDaemon)等。
當調用start方法后,線程開始執行run方法中的代碼。線程進入運行狀態。可以通過Thread類的isAlive方法來判斷線程是否處于運行狀態。當線程處于運行狀態時,isAlive返回true,當isAlive返回false時,可能線程處于等待狀態,也可能處于停止狀態。下面的代碼演示了線程的創建、運行和停止三個狀態之間的切換,并輸出了相應的isAlive返回值。
package chapter2;
public class LifeCycle extends Thread
{
public void run()
{
int n = 0;
while ((++n) < 1000);
}
public static void main(String[] args) throws Exception
{
LifeCycle thread1 = new LifeCycle();
System.out.println("isAlive: " + thread1.isAlive());
thread1.start();
System.out.println("isAlive: " + thread1.isAlive());
thread1.join(); // 等線程thread1結束后再繼續執行
System.out.println("thread1已經結束!");
System.out.println("isAlive: " + thread1.isAlive());
}
}
要注意一下,在上面的代碼中使用了join方法,這個方法的主要功能是保證線程的run方法完成后程序才繼續運行,這個方法將在后面的文章中介紹
上面代碼的運行結果:
isAlive: false
isAlive: true
thread1已經結束!
isAlive: false
二、掛起和喚醒線程
一但線程開始執行run方法,就會一直到這個run方法執行完成這個線程才退出。但在線程執行的過程中,可以通過兩個方法使線程暫時停止執行。這兩個方法是suspend和sleep。在使用suspend掛起線程后,可以通過resume方法喚醒線程。而使用sleep使線程休眠后,只能在設定的時間后使線程處于就緒狀態(在線程休眠結束后,線程不一定會馬上執行,只是進入了就緒狀態,等待著系統進行調度)。
雖然suspend和resume可以很方便地使線程掛起和喚醒,但由于使用這兩個方法可能會造成一些不可預料的事情發生,因此,這兩個方法被標識為deprecated(抗議)標記,這表明在以后的jdk版本中這兩個方法可能被刪除,所以盡量不要使用這兩個方法來操作線程。下面的代碼演示了sleep、suspend和resume三個方法的使用。
package chapter2;
public class MyThread extends Thread
{
class SleepThread extends Thread
{
public void run()
{
try
{
sleep(2000);
}
catch (Exception e)
{
}
}
}
public void run()
{
while (true)
System.out.println(new java.util.Date().getTime());
}
public static void main(String[] args) throws Exception
{
MyThread thread = new MyThread();
SleepThread sleepThread = thread.new SleepThread();
sleepThread.start(); // 開始運行線程sleepThread
sleepThread.join(); // 使線程sleepThread延遲2秒
thread.start();
boolean flag = false;
while (true)
{
sleep(5000); // 使主線程延遲5秒
flag = !flag;
if (flag)
thread.suspend();
else
thread.resume();
}
}
}
從表面上看,使用sleep和suspend所產生的效果類似,但sleep方法并不等同于suspend。它們之間最大的一個區別是可以在一個線程中通過suspend方法來掛起另外一個線程,如上面代碼中在主線程中掛起了thread線程。而sleep只對當前正在執行的線程起作用。在上面代碼中分別使sleepThread和主線程休眠了2秒和5秒。在使用sleep時要注意,不能在一個線程中來休眠另一個線程。如main方法中使用thread.sleep(2000)方法是無法使thread線程休眠2秒的,而只能使主線程休眠2秒。
在使用sleep方法時有兩點需要注意:
1. sleep方法有兩個重載形式,其中一個重載形式不僅可以設毫秒,而且還可以設納秒(1,000,000納秒等于1毫秒)。但大多數操作系統平臺上的Java虛擬機都無法精確到納秒,因此,如果對sleep設置了納秒,Java虛擬機將取最接近這個值的毫秒。
2. 在使用sleep方法時必須使用throws或try{...}catch{...}。因為run方法無法使用throws,所以只能使用try{...}catch{...}。當在線程休眠的過程中,使用interrupt方法(這個方法將在2.3.3中討論)中斷線程時sleep會拋出一個InterruptedException異常。sleep方法的定義如下:
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
三、終止線程的三種方法
有三種方法可以使終止線程。
1. 使用退出標志,使線程正常退出,也就是當run方法完成后線程終止。
2. 使用stop方法強行終止線程(這個方法不推薦使用,因為stop和suspend、resume一樣,也可能發生不可預料的結果)。
3. 使用interrupt方法中斷線程。
1. 使用退出標志終止線程
當run方法執行完后,線程就會退出。但有時run方法是永遠不會結束的。如在服務端程序中使用線程進行監聽客戶端請求,或是其他的需要循環處理的任務。在這種情況下,一般是將這些任務放在一個循環中,如while循環。如果想讓循環永遠運行下去,可以使用while(true){...}來處理。但要想使while循環在某一特定條件下退出,最直接的方法就是設一個boolean類型的標志,并通過設置這個標志為true或false來控制while循環是否退出。下面給出了一個利用退出標志終止線程的例子。
package chapter2;
public class ThreadFlag extends Thread
{
public volatile boolean exit = false;
public void run()
{
while (!exit);
}
public static void main(String[] args) throws Exception
{
ThreadFlag thread = new ThreadFlag();
thread.start();
sleep(5000); // 主線程延遲5秒
thread.exit = true; // 終止線程thread
thread.join();
System.out.println("線程退出!");
}
}
在上面代碼中定義了一個退出標志exit,當exit為true時,while循環退出,exit的默認值為false。在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit的值,
2. 使用stop方法終止線程
使用stop方法可以強行終止正在運行或掛起的線程。我們可以使用如下的代碼來終止線程:
thread.stop();
雖然使用上面的代碼可以終止線程,但使用stop方法是很危險的,就象突然關閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果,因此,并不推薦使用stop方法來終止線程。
3. 使用interrupt方法終止線程
使用interrupt方法來終端線程可分為兩種情況:
(1)線程處于阻塞狀態,如使用了sleep方法。
(2)使用while(!isInterrupted()){...}來判斷線程是否被中斷。
在第一種情況下使用interrupt方法,sleep方法將拋出一個InterruptedException例外,而在第二種情況下線程將直接退出。下面的代碼演示了在第一種情況下使用interrupt方法。
package chapter2;
public class ThreadInterrupt extends Thread
{
public void run()
{
try
{
sleep(50000); // 延遲50秒
}
catch (InterruptedException e)
{
System.out.println(e.getMessage());
}
}
public static void main(String[] args) throws Exception
{
Thread thread = new ThreadInterrupt();
thread.start();
System.out.println("在50秒之內按任意鍵中斷線程!");
System.in.read();
thread.interrupt();
thread.join();
System.out.println("線程已經退出!");
}
}
上面代碼的運行結果如下:
在50秒之內按任意鍵中斷線程!
sleep interrupted
線程已經退出!
在調用interrupt方法后, sleep方法拋出異常,然后輸出錯誤信息:sleep interrupted。
注意:在Thread類中有兩個方法可以判斷線程是否通過interrupt方法被終止。一個是靜態的方法interrupted(),一個是非靜態的方法isInterrupted(),這兩個方法的區別是interrupted用來判斷當前線是否被中斷,而isInterrupted可以用來判斷其他線程是否被中斷。因此,while (!isInterrupted())也可以換成while (!Thread.interrupted())。
Java多線程初學者指南(5):join方法的使用
在上面的例子中多次使用到了Thread
類的join
方法。我想大家可能已經猜出來join
方法的功能是什么了。對,join
方法的功能就是使異步執行的線程變成同步執行。也就是說,當調用線程實例的start
方法后,這個方法會立即返回,如果在調用start
方法后后需要使用一個由這個線程計算得到的值,就必須使用join
方法。如果不使用join
方法,就不能保證當執行到start
方法后面的某條語句時,這個線程一定會執行完。而使用join
方法后,直到這個線程退出,程序才會往下執行。下面的代碼演示了join
的用法。
package mythread;
public class JoinThread extends Thread
{
public static int n = 0;
static synchronized void inc()
{
n++;
}
public void run()
{
for (int i = 0; i < 10; i++)
try
{
inc();
sleep(3); // 為了使運行結果更隨機,延遲3毫秒
}
catch (Exception e)
{
}
}
public static void main(String[] args) throws Exception
{
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++) // 建立100個線程
threads[i] = new JoinThread();
for (int i = 0; i < threads.length; i++) // 運行剛才建立的100個線程
threads[i].start();
if (args.length > 0)
for (int i = 0; i < threads.length; i++) // 100個線程都執行完后繼續
threads[i].join();
System.out.println("n=" + JoinThread.n);
}
}
在例程2-8中建立了100個線程,每個線程使靜態變量n增加10。如果在這100個線程都執行完后輸出n,這個n值應該是1000。
1. 測試1
使用如下的命令運行上面程序:
java mythread.JoinThread
程序的運行結果如下:
n=442
這個運行結果可能在不同的運行環境下有一些差異,但一般n不會等于1000。從上面的結果可以肯定,這100個線程并未都執行完就將n輸出了。
2. 測試2
使用如下的命令運行上面的代碼:
在上面的命令行中有一個參數join,其實在命令行中可以使用任何參數,只要有一個參數就可以,這里使用join,只是為了表明要使用join方法使這100個線程同步執行。
程序的運行結果如下:
n=1000
無論在什么樣的運行環境下運行上面的命令,都會得到相同的結果:n=1000。這充分說明了這100個線程肯定是都執行完了,因此,n一定會等于1000。
Java多線程初學者指南(6):慎重使用volatile關鍵字
volatile關鍵字相信了解Java多線程的讀者都很清楚它的作用。volatile關鍵字用于聲明簡單類型變量,如int、float、boolean等數據類型。如果這些簡單數據類型聲明為volatile,對它們的操作就會變成原子級別的。但這有一定的限制。例如,下面的例子中的n就不是原子級別的:
package mythread;
public class JoinThread extends Thread
{
public static volatile int n = 0;
public void run()
{
for (int i = 0; i < 10; i++)
try
{
n = n + 1;
sleep(3); // 為了使運行結果更隨機,延遲3毫秒
}
catch (Exception e)
{
}
}
public static void main(String[] args) throws Exception
{
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++)
// 建立100個線程
threads[i] = new JoinThread();
for (int i = 0; i < threads.length; i++)
// 運行剛才建立的100個線程
threads[i].start();
for (int i = 0; i < threads.length; i++)
// 100個線程都執行完后繼續
threads[i].join();
System.out.println("n=" + JoinThread.n);
}
}
如果對n的操作是原子級別的,最后輸出的結果應該為n=1000,而在執行上面積代碼時,很多時侯輸出的n都小于1000,這說明n=n+1不是原子級別的操作。原因是聲明為volatile的簡單變量如果當前值由該變量以前的值相關,那么volatile關鍵字不起作用,也就是說如下的表達式都不是原子操作:
n = n + 1;
n++;
如果要想使這種情況變成原子操作,需要使用synchronized關鍵字,如上的代碼可以改成如下的形式:
package mythread;
public class JoinThread extends Thread
{
public static int n = 0;
public static synchronized void inc()
{
n++;
}
public void run()
{
for (int i = 0; i < 10; i++)
try
{
inc(); // n = n + 1 改成了 inc();
sleep(3); // 為了使運行結果更隨機,延遲3毫秒
}
catch (Exception e)
{
}
}
public static void main(String[] args) throws Exception
{
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++)
// 建立100個線程
threads[i] = new JoinThread();
for (int i = 0; i < threads.length; i++)
// 運行剛才建立的100個線程
threads[i].start();
for (int i = 0; i < threads.length; i++)
// 100個線程都執行完后繼續
threads[i].join();
System.out.println("n=" + JoinThread.n);
}
}
上面的代碼將n=n+1改成了inc(),其中inc方法使用了synchronized關鍵字進行方法同步。因此,在使用volatile關鍵字時要慎重,并不是只要簡單類型變量使用volatile修飾,對這個變量的所有操作都是原來操作,當變量的值由自身的上一個決定時,如n=n+1、n++等,volatile關鍵字將失效,只有當變量的值和自身上一個值無關時對該變量的操作才是原子級別的,如n = m + 1,這個就是原級別的。所以在使用volatile關鍵時一定要謹慎,如果自己沒有把握,可以使用synchronized來代替volatile。
posted on 2009-03-16 15:48
冬天出走的豬 閱讀(194)
評論(0) 編輯 收藏 所屬分類:
JAVA知識