聲明,本篇内容着重于筆記。内容多摘抄于網絡上線程大神的文章,我會盡量貼全引用位址。若是沒有您看到摘抄了關于您的文章并沒有填寫出處請聯系我。謝謝
摘抄: http://blog.csdn.net/axman
http://blog.csdn.net/budapest
http://blog.csdn.net/drifterj
目的
多線程程式設計的目的,就是"最大限度地利用CPU資源"
基礎概念
線程對象是可以産生線程的對象.比如在java平台中Thread對象,Runnable對象.在JAVA中,線程對象是JVM産生的一個普通的Object子類.
線程,是指正在執行的一個指令序列.在java平台上是指從一個線程對象的start()開始.運作run方法體中的那一段相對獨立的過程.線程是CPU配置設定給這個對象的一個運作過程.我們說的這個線程在幹什麼,不是說一個線程對象在幹什麼,而是這個運作過程在幹什麼.
并發(concurrent)是指.在單CPU系統中,系統排程在某一時刻隻能讓一個線程運作,雖然這種調試機制有多種形式(大多數是時間片輪巡為主),但無論如何,要通過不斷切換需要運作的線程讓其運作的方式就叫并發.
并行(parallel)是指在多CPU系統中,可以讓兩個以上的線程同時運作,這種可以同時讓兩個以上線程同時運作的方式叫做并行.
引入一個主要的方法. Thread.join(),看代碼片段
public class Thread03 {
public static void main(String[] args) {
SubThread t = new SubThread();
t.start();
// 線上程對象a上調用join()方法,就是讓目前正在執行的線程等待線程對象a對應的線程運作完成後
// 才繼續運作
try {
t.join(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("10");
}
public static class SubThread extends Thread{
public int i = 0 ;
@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
System.out.println(""+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
若是沒有調用join(),則根據單cpu單線程的運作機制,肯定是主線程運作完,再運作子線程,是以會列印 10 1 2 3... 9
增加join函數,運作後發現列印出 1 2 3...9 10 為什麼這一結果呢,看注釋. join有個設定等待的時間,假設join(10)意思是“喂,目前主線程,給我10ms用cpu時間”,那麼10ms過後,主線程拿到cpu完成自己的事兒,放開cpu,子線程繼續幹活。比如設定個10ms,那麼輸出結果也許是
join()預設不指定時間,則為0,0代表永遠等待,直到子線程死掉。另外時間不要為負的,沒有意義且會異常。
通過Thread執行個體的start(),一個Thread的執行個體隻能産生一個線程.
線程對象一旦Start() 就建立了一個線程,且該線程對象隻能産生一個線程。
JDK1.6源碼:
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0 || this != me)
throw new IllegalThreadStateException();
group.add(this);
start0();
if (stopBeforeStart) {
stop0(throwableFromStop);
}
}
我們看到threadStatus是關鍵點,是0,則代表新線程。
通過底層VM控制狀态值
/**
* Returns the state of this thread.
* This method is designed for use in monitoring of the system state,
* not for synchronization control.
*
* @return this thread's state.
* @since 1.5
*/
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
JDK1.5源碼 使用的是started
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see java.lang.Thread#run()
* @see java.lang.Thread#stop()
*/
public synchronized void start() {
if (started)
throw new IllegalThreadStateException();
started = true;
group.add(this);
start0();
}
有興趣再看看JDK 1.7源碼
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
意思都差不多把,我是這麼了解的,反正僅僅讓start()一次,建立一次線程。
那麼如果要在一個執行個體上産生多個線程(多個線程共同通路同一執行個體的一些共同資源),我們應該如何做呢?這就是Runnable接口給我們帶來的偉大的功能.
public class Runnable01 {
public static void main(String[] args) {
//正如它的名字一樣,Runnable的執行個體是可運作的,但它自己并不能直接運作,它需要被Thread對象來
//包裝才行運作
MyRunnable r = new MyRunnable();
for (int i = 0; i < 3; i++) {
new Thread(r).start();
}
}
public static class MyRunnable implements Runnable{
int x = 0;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(x++);
}
}
}
}
輸出結果是 0 1 2 .... 28 29
這就說明了,3個線程,都在同一個 r 對象上執行,使用的是同一個x對象。同一執行個體(Runnable執行個體)的多個線程.。注意,因為這個例子是在單CPU上運作的,是以沒有對多個線程同時操作共同的對象進行同步.這裡是為了說明的友善而簡化了同步,而真正的環境中你無法預知程式會在什麼環境下運作,是以一定要考慮同步.
一個線程對象生成後,如果要産生一個執行的線程,就一定要調用它的start()方法.在介紹這個方法時不得不同時說明run方法.其實線程對象的run方法完全是一個接口回調方法,它是你這個線程對象要完成的具體邏輯.簡單說你要做什麼就你在run中完成,而如何做,什麼時候做就不需要你控制了,你隻要調用start()方法,JVM就會管理這個線程對象讓它産生一個線程并注冊到線程處理系統中(線程棧).
從表面上看,start()方法調用了run()方法,事實上,start()方法并沒有直接調用run方法.在JDK1.5以前start()方法是本地方法,它如何最終調用run方法已經不是JAVA程式員所能了解的.而在JDK1.5中,原來的那個本地start()方法被start0()代替,另個一個純JAVA的start()中調用本地方法start0(),而在start()方法中做了一個驗證,就是對一個全局變量(對象變量)started做檢驗,如果為true,則start()抛出異常,不會調用本地方法start0(),否則,先将該變量設有true,然後調用start0()
從中我們可以看到這個為了控制一個線程對象隻能運作成功一次start()方法.這是因為線程的運作要擷取目前環境,包括安全,父線程的權限,優先級等條件,如果一個線程對象可以運作多次,那麼定義一個static的線程在一個環境中擷取相應權限和優先級,運作完成後它在另一個環境中利用原來的權限和優先級等屬性在目前環境中運作,這樣就造成無法預知的結果.簡單說來,讓一個線程對象隻能成功運作一次,是基于對線程管理的需要.
start()方法最本質的功能是從CPU中申請另一個線程空間來執行run()方法中的代碼,它和目前的線程是兩條線,在相對獨立的線程空間運作,也就是說,如果你直接調用線程對象的run()方法,當然也會執行,但那是在目前線程中執行,run()方法執行完成後繼續執行下面的代碼.而調用start()方法後,run()方法的代碼會和目前線程并發(單CPU)或并行(多CPU)執行.是以請記住一句話[調用線程對象的run方法不會産生一個新的線程],雖然可以達到相同的執行結果,但執行過程和執行效率不同.
下面介紹一些線程中重要的方法 interrupt() interrupted() isInterrupted()
首先看源碼:
interrupt()
/**
* Interrupts this thread.
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
*
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel </code>interruptible
* channel<code>} then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p>
*
* <p> Interrupting a thread that is not alive need not have any effect.
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
* 注意标紅的,隻是改變中斷辨別,實際線程還是在跑的
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); <span style="color:#CC0000;">// Just to set the interrupt flag</span>
b.interrupt();
return;
}
}
interrupt0();
}
interrupted()
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> <span style="color:#CC0000;">if the current thread has been interrupted; </span>
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
* 上述大概意思是說,你隻要了解傳回ture代表,這個線程之前被interupt就行.
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
isinterrupted()
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> <span style="color:#CC0000;">if this thread has been interrupted;</span>
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
大多數人以為,一個線程象調用了interrupt()方法,那它對應的線程就應該被中斷而抛出異常,事實中,當一個線程對象調用interrupt()方法,它對應的線程并沒有被中斷,隻是改變了它的中斷狀态.使目前線程的狀态變以中斷狀态,如果沒有其它影響,線程還會自己繼續執行. 隻有當線程執行到sleep,wait,join等方法時,或者自己檢查中斷狀态而抛出異常的情況下,線程才會抛出異常.
如果線程對象調用interrupt()後它對應的線程就立即中斷,那麼interrupted()方法就不可能執行.因為interrupted()方法是一個static方法,就是說隻能在目前線程上調用,而如果一個線程interrupt()後它已經中斷了,那它又如何讓自己interrupted()?
以上總結一句話: 沒有占用CPU運作的線程是不可能給自己的中斷狀态置位的,也不可能調用interrupted()
正因為一個線程調用interrupt()後隻是改變了中斷狀态,它可以繼續執行下去,在沒有調用sleep,wait,join等法或自己抛出異常之前,它就可以調用interrupted()來清除中斷狀态(還會原狀) interrupted()方法會檢查目前線程的中斷狀态,如果為"被中斷狀态"則改變目前線程為"非中斷狀态"并傳回true,如果為"非中斷狀态"則傳回false,它不僅檢查目前線程是否為中斷狀态,而且在保證目前線程回來非中斷狀态,是以它叫"interrupted",是說中斷的狀态已經結束(到非中斷狀态了) isInterrupted()方法則僅僅檢查線程對象對應的線程是否是中斷狀态,并不改變它的狀态.
(1) Thread.stop(), Thread.suspend(), Thread.resume() 和Runtime.runFinalizersOnExit() 這些終止線程運作的方法 。這些方法已經被廢棄,使用它們是極端不安全的。
(2) Thread.interrupt() 方法是很好的選擇。但是使用的時候我們必須好好了解一下它的用處。
//無法中斷正在運作的線程代碼
class TestRunnable implements Runnable{
public void run(){
while(true)
{
System.out.println( "Thread is running..." );
long time = System.currentTimeMillis();//去系統時間的毫秒數
while((System.currentTimeMillis()-time < 1000)) {
//程式循環1秒鐘,不同于sleep(1000)會阻塞程序。
}
}
}
}
public class ThreadDemo{
public static void main(String[] args){
Runnable r=new TestRunnable();
Thread th1=new Thread(r);
th1.start();
th1.interrupt();
}
}
/運作結果:一秒鐘列印一次Thread is running...。程式沒有終止的任何迹象
上面的代碼說明interrupt()并沒有中斷一個正在運作的線程,或者說讓一個running中的線程放棄CPU。那麼interrupt到底中斷什麼。
首先我們看看interrupt究竟在幹什麼。
當我們調用th1.interrput()的時候,線程th1的中斷狀态(interrupted status) 會被置位。我們可以通過Thread.currentThread().isInterrupted() 來檢查這個布爾型的中斷狀态。
在Core Java中有這樣一句話:"沒有任何語言方面的需求要求一個被中斷的程式應該終止。中斷一個線程隻是為了引起該線程的注意,被中斷線程可以決定如何應對中斷 "。好好體會這句話的含義,看看下面的代碼:
//Interrupted的經典使用代碼
public void run(){
try{
....
while(!Thread.currentThread().isInterrupted()&& more work to do){
// do more work;
}
}catch(InterruptedException e){
// thread was interrupted during sleep or wait
}
finally{
// cleanup, if required
}
}
很顯然,在上面代碼中,while循環有一個決定因素就是需要不停的檢查自己的中斷狀态。當外部線程調用該線程的interrupt 時,使得中斷狀态置位。這是該線程将終止循環,不在執行循環中的do more work了。
這說明: interrupt中斷的是線程的某一部分業務邏輯,前提是線程需要檢查自己的中斷狀态(isInterrupted())。
但是當th1被阻塞的時候,比如被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞時。調用它的interrput()方法。可想而知,沒有占用CPU運作的線程是不可能給自己的中斷狀态置位的。這就會産生一個InterruptedException異常。
//中斷一個被阻塞的線程代碼
class TestRunnable implements Runnable{
public void run(){
try{
Thread.sleep(1000000); //這個線程将被阻塞1000秒
}catch(InterruptedException e){
e.printStackTrace();
//do more work and return.
}
}
}
public class TestDemo2{
public static void main(String[] args) {
Runnable tr=new TestRunnable();
Thread th1=new Thread(tr);
th1.start(); //開始執行分線程
while(true){
th1.interrupt(); //中斷這個分線程
}
}
}
/*運作結果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at TestRunnable.run(TestDemo2.java:4)
at java.lang.Thread.run(Unknown Source)*/
* 如果線程被阻塞,它便不能核查共享變量,也就不能停止。這在許多情況下會發生,例如調用
* Object.wait()、ServerSocket.accept()和DatagramSocket.receive()時,他們都可能永
* 久的阻塞線程。即使發生逾時,在逾時期滿之前持續等待也是不可行和不适當的,是以,要使
* 用某種機制使得線程更早地退出被阻塞的狀态。很不幸運,不存在這樣一種機制對所有的情況
* 都适用,但是,根據情況不同卻可以使用特定的技術。使用Thread.interrupt()中斷線程正
* 如Example1中所描述的,Thread.interrupt()方法不會中斷一個正在運作的線程。這一方法
* 實際上完成的是,線上程受到阻塞時抛出一個中斷信号,這樣線程就得以退出阻塞的狀态。更
* 确切的說,如果線程被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那麼,
* 它将接收到一個中斷異常(InterruptedException),進而提早地終結被阻塞狀态。
繼續介紹幾種重要方法 sleep() join() yield()
首先看源碼:
/**
* Causes the currently executing thread object to temporarily pause
* and allow other threads to execute.
*/
public static native void yield();
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis the length of time to sleep in milliseconds.
* @exception InterruptedException if any thread has interrupted
* the current thread. The <i>interrupted status</i> of the
* current thread is cleared when this exception is thrown.
* @see Object#notify()
*/
public static native void sleep(long millis) throws InterruptedException;
/**
* Waits for this thread to die.
*
* @exception InterruptedException if any thread has interrupted
* the current thread. The <i>interrupted status</i> of the
* current thread is cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
Sleep() sleep()方法中是類方法,也就是對目前線程而言的,程式員不能指定某個線程去sleep,隻能是目前線程執行到sleep()方法時,睡眠指定的時間(讓其它線程運作).事實上也隻能是類方法,在目前線程上調用.試想如果你調用一個線程對象的sleep()方法,那麼這個對象對應的線程如果不是正在運作,它如何sleep()?是以隻有目前線程,因為它正在執行,你才能保證它可以調用sleep()方法.
原則:[在synchronized同步方法中盡量不要調用線程的sleep()方法,因為它并不會釋放對象鎖,其他對象仍然不能通路共享資料],或者簡單說, 對于一般水準的程式員你基本不應該調用sleep()方法.
使目前線程(即調用該方法的線程)暫停執行一段時間,讓其他線程有機會繼續執行,但它并不釋放對象鎖。也就是說如果有synchronized同步快,其他線程仍然不能通路共享資料。注意該方法要捕捉異常。
例如有兩個線程同時執行(沒有synchronized)一個線程優先級為MAX_PRIORITY,另一個為MIN_PRIORITY,如果沒有Sleep()方法,隻有高優先級的線程執行完畢後,低優先級的線程才能夠執行;但是高優先級的線程sleep(500)後,低優先級就有機會執行了。
總之,sleep()可以使低優先級的線程得到執行的機會,當然也可以讓同優先級、高優先級的線程有執行的機會。
join() 是若場景為: A調用完傳回的結果是OK,則繼續執行B否則不執行。 那麼就好像是個隊列,有一環斷,整個任務失敗。那麼代碼就可以寫為
public class Thread03 {
public static void main(String[] args) {
SubThread t = new SubThread();
t.start();
// 線上程對象a上調用join()方法,就是讓目前正在執行的線程等待線程對象a對應的線程運作完成後
// 才繼續運作
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("10");
}
public static class SubThread extends Thread{
public int i = 0 ;
@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
System.out.println(""+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
<span style="font-size:12px;">輸出結果 </span>0.1,2,3....9,10 主線程等待子線程完成後,再做。
原則 :[join是測試其它工作狀态的唯一正确方法],我見過很多人,甚至有的是博士生,在處理一項工作時如果另一項工作沒有完成,說讓目前工作線程sleep(x),我問他,你這個x是如何指定的,你怎麼知道是100毫秒而不是99毫秒或是101毫秒?其實這就是OnXXX事件的實質,我們不是要等多長時間才去做什麼事,而是當等待的工作正好完成的時候去做.
yield()
該方法與sleep()類似,隻是不能由使用者指定暫停多長時間,并且yield()方法隻能讓同優先級的線程有執行的機會。
原則:[不是非常必要的情況下,沒有理由調用它].調用這個方法不會提高任何效率,隻是降低了CPU的總周期。
wait()和notify()、notifyAll()
這三個方法可能一時半會兒有點無法了解,但是一定要記住下面兩句話
wait(),notify()/notityAll()方法是普通對象的方法(Object超類中實作),而不是線程對象的方法]
[wait(),notify()/notityAll()方法隻能在同步方法中調用]
我們來看下源碼:
/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the <code>wait</code> methods.
* <p>
* The awakened thread will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened thread will
* compete in the usual manner with any other threads that might be
* actively competing to synchronize on this object; for example, the
* awakened thread enjoys no reliable privilege or disadvantage in being
* the next thread to lock this object.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. A thread becomes the owner of the
* object's monitor in one of three ways:
* <ul>
* <li>By executing a synchronized instance method of that object.
* <li>By executing the body of a <code>synchronized</code> statement
* that synchronizes on the object.
* <li>For objects of type <code>Class,</code> by executing a
* synchronized static method of that class.
* </ul>
* <p>
* Only one thread at a time can own an object's monitor.
*
* @exception IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
*/
public final native void notify();
/**
* Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the
* <code>wait</code> methods.
* <p>
* The awakened threads will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened threads
* will compete in the usual manner with any other threads that might
* be actively competing to synchronize on this object; for example,
* the awakened threads enjoy no reliable privilege or disadvantage in
* being the next thread to lock this object.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the <code>notify</code> method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @exception IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notify()
* @see java.lang.Object#wait()
*/
public final native void notifyAll();
/**
* Causes the current thread to wait until either another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or a
* specified amount of time has elapsed.
* <p>
* The current thread must own this object's monitor.
* <p>
* This method causes the current thread (call it <var>T</var>) to
* place itself in the wait set for this object and then to relinquish
* any and all synchronization claims on this object. Thread <var>T</var>
* becomes disabled for thread scheduling purposes and lies dormant
* until one of four things happens:
* <ul>
* <li>Some other thread invokes the <tt>notify</tt> method for this
* object and thread <var>T</var> happens to be arbitrarily chosen as
* the thread to be awakened.
* <li>Some other thread invokes the <tt>notifyAll</tt> method for this
* object.
* <li>Some other thread {@linkplain Thread#interrupt() interrupts}
* thread <var>T</var>.
* <li>The specified amount of real time has elapsed, more or less. If
* <tt>timeout</tt> is zero, however, then real time is not taken into
* consideration and the thread simply waits until notified.
* </ul>
* The thread <var>T</var> is then removed from the wait set for this
* object and re-enabled for thread scheduling. It then competes in the
* usual manner with other threads for the right to synchronize on the
* object; once it has gained control of the object, all its
* synchronization claims on the object are restored to the status quo
* ante - that is, to the situation as of the time that the <tt>wait</tt>
* method was invoked. Thread <var>T</var> then returns from the
* invocation of the <tt>wait</tt> method. Thus, on return from the
* <tt>wait</tt> method, the synchronization state of the object and of
* thread <tt>T</tt> is exactly as it was when the <tt>wait</tt> method
* was invoked.
* <p>
* A thread can also wake up without being notified, interrupted, or
* timing out, a so-called <i>spurious wakeup</i>. While this will rarely
* occur in practice, applications must guard against it by testing for
* the condition that should have caused the thread to be awakened, and
* continuing to wait if the condition is not satisfied. In other words,
* waits should always occur in loops, like this one:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait(timeout);
* ... // Perform action appropriate to condition
* }
* </pre>
* (For more information on this topic, see Section 3.2.3 in Doug Lea's
* "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
* 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
* Language Guide" (Addison-Wesley, 2001).
*
* <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
* interrupted} by any thread before or while it is waiting, then an
* <tt>InterruptedException</tt> is thrown. This exception is not
* thrown until the lock status of this object has been restored as
* described above.
*
* <p>
* Note that the <tt>wait</tt> method, as it places the current thread
* into the wait set for this object, unlocks only this object; any
* other objects on which the current thread may be synchronized remain
* locked while the thread waits.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the <code>notify</code> method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @param timeout the maximum time to wait in milliseconds.
* @exception IllegalArgumentException if the value of timeout is
* negative.
* @exception IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
* @exception InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final native void wait(long timeout) throws InterruptedException;
這三個方法用于協調多個線程對共享資料的存取,是以必須在synchronized語句塊内使用。synchronized關鍵字用于保護共享資料,阻止其他線程對共享資料的存取,但是這樣程式的流程就很不靈活了,如何才能在目前線程還沒退出synchronized資料塊時讓其他線程也有機會通路共享資料呢?此時就用這三個方法來靈活控制。
wait()方法使目前線程暫停執行并釋放對象鎖标示,讓其他線程可以進入synchronized資料塊,目前線程被放入對象等待池中。當調用notify()方法後,将從對象的等待池中移走一個任意的線程并放到鎖标志等待池中,隻有鎖标志等待池中線程能夠擷取鎖标志;如果鎖标志等待池中沒有線程,則notify()不起作用。
notifyAll()則從對象等待池中移走所有等待那個對象的線程并放到鎖标志等待池中。
[線程的互斥控制]
多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀态,而該狀态會影響
另一線程對該對象的真正結果.
這個例子我們在太多的文檔中可以看到,就象兩個操售票員同時售出同一張票一樣.
線程A 線程B
1.線程A在資料庫中查詢存票,發現票C可以賣出
2.線程A接受使用者訂票請求,準備出票.
3. 這時切換到了線程B執行
4. 線程B在資料庫中查詢存票,發現票C可以賣出
5. 線程B将票賣了出去
6.切換到線程A執行,線程A賣了一張已經賣出的票
是以需要一種機制來管理這類問題的發生,當某個線程正在執行一個不可分割的部分時,其它線程不能
不能同時執行這一部分.
像這種控制某一時刻隻能有一個線程執行某個執行單元的機制就叫互斥控制或共享互斥(mutual exclusion)
在JAVA中,用synchronized關鍵字來實作互斥控制(暫時這樣認為,JDK1.5已經發展了新的機制)
[synchornized關鍵字]
把一個單元聲明為synchronized,就可以讓在同一時間隻有一個線程操作該方法.有人說synchornized就是一把鎖,事實上它确實存在鎖,但是是誰的鎖,鎖誰,這是一個非常複雜的問題.每個對象隻有一把監視鎖(monitor lock),一次隻能被一個線程擷取.當一個線程擷取了這一個鎖後,其它線程就隻能等待這個線程釋放鎖才能再擷取.
那麼synchronized關鍵字到底鎖什麼?得到了誰的鎖?
對于同步塊,synchronized擷取的是參數中的對象鎖:
synchronized(obj){
//...............
}
線程執行到這裡時,首先要擷取obj這個執行個體的鎖,如果沒有擷取到線程隻能等待.如果多個線程
執行到這裡,隻能有一個線程擷取obj的鎖,然後執行{}中的語句,是以,obj對象的作用範圍不同,控制程式
不同.
假如:
public void test(){
Object o = new Object();
synchronized(obj){
//...............
}
}
這段程式控制不了任何,多個線程之間執行到Object o = new Object();時會各自産生一個對象然後擷取這個對象有監視鎖,各自皆大歡喜地執行.
而如果是類的屬性:
class Test{
Object o = new Object();
public void test(){
synchronized(o){
//...............
}
}
}
所有執行到Test執行個體的synchronized(o)的線程,隻有一個線程可以擷取到監視鎖.
有時我們會這樣:
public void test(){
synchronized(this){
//...............
}
}
那麼所有執行Test執行個體的線程隻能有一個線程執行.而synchornized(o)和
synchronized(this)的範圍是不同的,因為執行到Test執行個體的synchornized(o)的線程等待時,其它線程可以執行
Test執行個體的synchronized(o1)部分,但多個線程同時隻有一個可以執行Test執行個體的synchornized(this).
而對于 synchronized(Test.class){
//...............
}這樣的同步塊而言,所有調用Test多個執行個體的線程賜教隻能有一個線程可以執行.
[synchronized方法]
如果一個方法聲明為synchronized的,則等同于把在為個方法上調用synchronized(this).
如果一個靜态方法被聲明為synchronized,則等同于把在為個方法上調用synchronized(類.class).
現在進入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object對象的方法,而不是
線程對象的方法.如同鎖一樣,它們是線上程中調用某一對象上執行的.
class Test{
public synchronized void test(){
//擷取條件,int x 要求大于100;
if(x < 100)
wait();
}
}
這裡為了說明方法沒有加在try{}catch(){}中,如果沒有明确在哪個對象上調用wait()方法,則
為this.wait();
假如:
Test t = new Test();
現在有兩個線程都執行到t.test();方法.其中線程A擷取了t的對象鎖,進入test()方法内.
這時x小于100,是以線程A進入等待.
當一個線程調用了wait方法後,這個線程就進入了這個對象的休息室(waitset),這是一個虛拟的對象,但JVM中一定存在這樣的一個資料結構用來記錄目前對象中有哪些程線程在等待.當一個線程進入等待時,它就會釋放鎖,讓其它線程來擷取這個鎖.是以線程B有機會獲得了線程A釋放的鎖,進入test()方法,如果這時x還是小于100,線程B也進入了t的休息室.這兩個線程隻能等待其它線程調用notity[All]來喚醒.
但是如果調用的是有參數的wait(time)方法,則線程A,B都會在休息室中等待這個時間後自動喚醒.
[為什麼真正的應用都是用while(條件)而不用if(條件)]
在實際的程式設計中我們看到大量的例子都是用
while(x < 100)
wait();go();而不是用if,為什麼呢?
在多個線程同時執行時,if(x <100)是不安全的.因為如果線程A和線程B都在t的休息室中等待,這時另一個線程使x==100了,并調用notifyAll方法,線程A繼續執行下面的go().而它執行完成後,x有可能又小于100,比如下面的程式中調用了--x,這時切換到線程B,線程B沒有繼續判斷,直接執行go();就産生一個錯誤的條件,隻有while才能保證線程B又繼續檢查一次.
[notify/notifyAll方法]
這兩個方法都是把某個對象上休息區内的線程喚醒,notify隻能喚醒一個,但究竟是哪一個不能确定,而notifyAll則喚醒這個對象上的休息室中所有的線程 一般有為了安全性,我們在絕對多數時候應該使用notifiAll(),除非你明确知道隻喚醒其中的一個線程. 那麼是否是隻要調用一個對象的wait()方法,目前線程就進入了這個對象的休息室呢?事實中,要調用一個對象的wait()方法,隻有目前線程擷取了這個對象的鎖,換句話說一定要在這個對象的同步方法或以這個對象為參數的同步塊中.
class MyThread extends Thread{
Test t = new Test();
public void run(){
t.test();
System.out.println("Thread say:Hello,World!");
}
}
public class Test {
int x = 0;
public void test(){
if(x==0)
try{
wait();
}catch(Exception e){}
}
public static void main(String[] args) throws Exception{
new MyThread().start();
}
}
這個線程就不會進入t的wait方法而直接列印出Thread say:Hello,World!.而如果改成
public class Test {
int x = 0;
public synchronized void test(){
if(x==0)
try{
wait();
}catch(Exception e){}
}
public static void main(String[] args) throws Exception{
new MyThread().start();
}
}
還記得開始時我們要必須記住的兩個原則質之一嗎[wait(),notify()/notityAll()方法隻能在同步方法中調用] 一旦加入了synchronized,标記為同步方法,則進入他修飾的這段代碼内的線程先去擷取一個特定對象的鎖定标示,并且虛拟機保證這個标示一次隻能被一條線程擁有。
我們就可以看到線程一直等待,注意這個線程進入等待後沒有其它線程喚醒,除非強行退出
JVM環境,否則它一直等待.是以請記住:
[線程要想調用一個對象的wait()方法就要先獲得該對象的監視鎖,而一旦調用wait()後又立即釋放該鎖]
線程互斥的經典例子是 售火車票
線程同步的經典例子是 生産者與消費者
下面分别請看:
線程間互斥應對的是這種場景:多個線程操作同一個資源(即某個對象),為保證線程在對資源的狀态(即對象的成員變量)進行一些非原子性操作後,狀态仍然是正确的。典型的例子是“售票廳售票應用”。售票廳剩餘100張票,10個視窗去賣這些票。這10個視窗,就是10條線程,售票廳就是他們共同操作的資源,其中剩餘的100張票就是這個資源的一個狀态。線程買票的過程就是去遞減這個剩餘數量的過程。不進行互斥控制的代碼如下:
package cn.test;
public class TicketOffice {
private int ticketNum = 0;
public TicketOffice(int ticketNum) {
super();
this.ticketNum = ticketNum;
}
public int getTicketNum() {
return ticketNum;
}
public void setTicketNum(int ticketNum) {
this.ticketNum = ticketNum;
}
/**
* 售票廳賣票的方法,這個方法操作了售票廳對象唯一的狀态--剩餘火車票數量。
* 該方法此處并未進行互斥控制。
*/
public void sellOneTicket(){
ticketNum--;
// 列印剩餘票的數量
if(ticketNum >= 0){
System.out.println("售票成功,剩餘票數: " + ticketNum);
}else{
System.out.println("售票失敗,票已售罄!");
}
}
public static void main(String[] args) {
final TicketOffice ticketOffice = new TicketOffice(100);
// 啟動10個線程,即10個視窗開始賣票
for(int i=0;i<10;i++){
new Thread(new Runnable(){
@Override
public void run() {
// 當還有剩餘票的時候,就去執行
while(ticketOffice.getTicketNum() > 0){
ticketOffice.sellOneTicket();
}
}
}).start();
}
}
}
最後列印的部分結果如下:
售票成功,剩餘票數: 93
售票成功,剩餘票數: 92
售票成功,剩餘票數: 91
售票成功,剩餘票數: 95
售票成功,剩餘票數: 96
售票成功,剩餘票數: 87
售票成功,剩餘票數: 86
售票成功,剩餘票數: 88
售票成功,剩餘票數: 89
售票成功,剩餘票數: 83
售票成功,剩餘票數: 82
售票成功,剩餘票數: 81
售票成功,剩餘票數: 90
售票成功,剩餘票數: 79
售票成功,剩餘票數: 93
可以看到 售票廳資源的狀态:剩餘票的數量,是不正确的。數量忽大忽小,這就是對統一資源進行操作沒有控制互斥的結果。
互斥操作的控制,Java提供了關鍵字synchronized進行的。synchronized可以修飾方法,也可以修飾代碼段。其代表的含義就是:進入他修飾的這段代碼内的線程必須先去擷取一個特定對象的鎖定标示,并且虛拟機保證這個标示一次隻能被一條線程擁有。通過這兩種方式修改上述代碼的方法sellOneTicket(),如下:
/**
* 已經進行了互斥控制。這裡是通過synchronized修飾整個方法實作的。
* 線程想進入這個方法,必須擷取目前對象的鎖定表示!
*/
public synchronized void sellOneTicket(){
ticketNum--;
// 列印剩餘票的數量
if(ticketNum >= 0){
System.out.println("售票成功,剩餘票數: " + ticketNum);
}else{
System.out.println("售票失敗,票已售罄!");
}
}
/**
* 已經進行了互斥控制。這裡是通過synchronized修飾代碼塊實作的。線程要想進入修飾的代碼塊,
* 必須擷取lock對象的對象标示。
*/
private Object lock = new Object();
public void sellOneTicket2(){
synchronized(lock){
ticketNum--;
// 列印剩餘票的數量
if(ticketNum >= 0){
System.out.println("售票成功,剩餘票數: " + ticketNum);
}else{
System.out.println("售票失敗,票已售罄!");
}
}
}
通過互斥控制後的輸出為:非常整齊,不會出現任何狀态不對的情況。
售票成功,剩餘票數: 99
售票成功,剩餘票數: 98
售票成功,剩餘票數: 97
售票成功,剩餘票數: 96
售票成功,剩餘票數: 95
售票成功,剩餘票數: 94
售票成功,剩餘票數: 93
售票成功,剩餘票數: 92
售票成功,剩餘票數: 91
售票成功,剩餘票數: 90
售票成功,剩餘票數: 89
售票成功,剩餘票數: 88
售票成功,剩餘票數: 87
售票成功,剩餘票數: 86
售票成功,剩餘票數: 85
售票成功,剩餘票數: 84
售票成功,剩餘票數: 83
售票成功,剩餘票數: 82
同步的概念再于線程間通信,比較典型的例子就是“生産者-消費者問題”。
多個生産者和多個消費者就是多條執行線程,他們共同操作一個資料結構中的資料,資料結構中有時是沒有資料的,這個時候消費者應該處于等待狀态而不是不斷的去通路這個資料結構。這裡就涉及到線程間通信(當然此處還涉及到互斥,這裡暫不考慮這一點),消費者線程一次消費後發現資料結構空了,就應該處于等待狀态,生産者生産資料後,就去喚醒消費者線程開始消費。生産者線程某次生産後發現資料結構已經滿了,也應該處于等待狀态,消費者消費一條資料後,就去喚醒生産者繼續生産。
實作這種線程間同步,可以通過Object類提供的wait,notify, notifyAll 3個方法去進行即可。一個簡單的生産者和消費者的例子代碼為:
package cn.test;
public class ProducerConsumer {
public static void main(String[] args) {
final MessageQueue mq = new MessageQueue(10);
// 建立3個生産者
for(int p=0;p<3;p++){
new Thread(new Runnable(){
@Override
public void run() {
while(true){
mq.put("消息來了!");
// 生産消息後,休息100毫秒
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Producer" + p).start();
}
// 建立3個消費者
for(int s=0;s<3;s++){
new Thread(new Runnable(){
@Override
public void run() {
while(true){
mq.get();
// 消費消息後,休息100毫秒
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Consumer" + s).start();
}
}
/**
* 内部類模拟一個消息隊列,生産者和消費者就去操作這個消息隊列
*/
private static class MessageQueue{
private String[] messages;// 放置消息的資料結構
private int opIndex; // 将要操作的位置索引
public MessageQueue(int size) {
if(size <= 0){
throw new IllegalArgumentException("消息隊列的長度至少為1!");
}
messages = new String[size];
opIndex = 0;
}
public synchronized void put(String message){
// Java中存線上程假醒的情況,此處用while而不是用if!可以參考Java規範!
while(opIndex == messages.length){
// 消息隊列已滿,生産者需要等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
messages[opIndex] = message;
opIndex++;
System.out.println("生産者 " + Thread.currentThread().getName() + " 生産了一條消息: " + message);
// 生産後,對消費者進行喚醒
notifyAll();
}
public synchronized String get(){
// Java中存線上程假醒的情況,此處用while而不是用if!可以參考Java規範!
while(opIndex == 0){
// 消息隊列無消息,消費者需要等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String message = messages[opIndex-1];
opIndex--;
System.out.println("消費者 " + Thread.currentThread().getName() + " 消費了一條消息: " + message);
// 消費後,對生産者進行喚醒
notifyAll();
return message;
}
}
}
一次輸出為:
<span style="font-size:12px;"> 消費者 Consumer1 消費了一條消息: 消息來了!
生産者 Producer0 生産了一條消息: 消息來了!
消費者 Consumer0 消費了一條消息: 消息來了!
生産者 Producer2 生産了一條消息: 消息來了!
消費者 Consumer2 消費了一條消息: 消息來了!
生産者 Producer1 生産了一條消息: 消息來了!
消費者 Consumer0 消費了一條消息: 消息來了!
生産者 Producer0 生産了一條消息: 消息來了!
消費者 Consumer1 消費了一條消息: 消息來了!
生産者 Producer2 生産了一條消息: 消息來了!
消費者 Consumer2 消費了一條消息: 消息來了!
生産者 Producer0 生産了一條消息: 消息來了!
消費者 Consumer1 消費了一條消息: 消息來了!
生産者 Producer1 生産了一條消息: 消息來了!
消費者 Consumer0 消費了一條消息: 消息來了!
生産者 Producer2 生産了一條消息: 消息來了!
消費者 Consumer0 消費了一條消息: 消息來了!
生産者 Producer1 生産了一條消息: 消息來了!
生産者 Producer0 生産了一條消息: 消息來了!
消費者 Consumer2 消費了一條消息: 消息來了!
消費者 Consumer1 消費了一條消息: 消息來了!
生産者 Producer1 生産了一條消息: 消息來了! </span>
多線程應用中,同步與互斥用的特别廣泛,這兩個是必須要了解并掌握的!
實戰
多線程程式設計的一般原則.
[安全性]是多線程程式設計的首要原則,如果兩個以上的線程通路同一對象時,一個線程會損壞另一個線程的資料,這就是違反了安全性原則,這樣的程式是不能進入實際應用的.安全性的保證可以通過設計安全的類和程式員的手工控制.如果多個線程對同一對象通路不會危及安全性,這樣的類就是線程安全的類,在JAVA中比如String類就被設計為線程安全的類.而如果不是線程安全的類,那麼就需要程式員在通路這些類的執行個體時手工控制它的安全性.
[可行性]是多線程程式設計的另一個重要原則,如果僅僅實作了安全性,程式卻在某一點後不能繼續執行或者多個線程發生死鎖,那麼這樣的程式也不能作為真正的多線程程式來應用.相對而言安全性和可行性是互相抵觸的,安全性越高的程式,可性行會越低.要綜合平衡.
[高性能] 多線程的目的本來就是為了增加程式運作的性能,如果一個多線程完成的工作還不
如單線程完成得快.那就不要應用多線程了.
高性能程式主要有以下幾個方面的因素:
資料吞吐率,在一定的時間内所能完成的處理能力.
響應速度,從送出請求到收到響應的時間.
容量,指同時處理雅緻同任務的數量.
安全性和可行性是必要條件,如果達到不這兩個原則那就不能稱為真正的多線程程式.
而高性是多線程程式設計的目的,也可以說是充要條件.否則,為什麼采用多線程程式設計呢?
[生産者與消費者模式]
首先以一個生産者和消費者模式來進入實戰篇的第一節.
生産者和消費者模式中保護的是誰?
多線程程式設計都在保護着某些對象,這個些對象是"緊俏資源",要被最大限度地利用,這也是
采用多線程方式的理由.在生産者消費者模式中,我們要保護的是"倉庫",在我下面的這個例子中,
就是桌子(table)
我這個例子的模式完全是生産者-消費者模式,但我換了個名字.廚師-食客模式,這個食
堂中隻有1張桌子,同時最多放10個盤子,現在有4個廚師做菜,每做好一盤就往桌子上放(生産者将
産品往倉庫中放),而有6個食客不停地吃(消費者消費産品,為了說明問題,他們的食量是無限的).
一般而言,廚師200-400ms做出一盤菜,而食客要400-600ms吃完一盤.當桌子上放滿了10
個盤子後,所有廚師都不能再往桌子上放,而當桌子是沒有盤子時,所有的食客都隻好等待.
下面我們來設計這個程式:
因為我們不知道具體是什麼菜,是以叫它food:
class Food{}
然後是桌子,因為它要有序地放而且要有序地取(不能兩個食客同時争取第三盤菜),是以我們
擴充LinkedList,或者你用聚合把一個LinkedList作為屬性也能達到同樣的目的,例子中我是用
繼承,從構造方法中傳入一個可以放置的最大值
class Table extends java.util.LinkedList{
int maxSize;
public Table(int maxSize){
this.maxSize = maxSize;
}
}
現在我們要為它加兩個方法,一是廚師往上面放菜的方法,一是食客從桌子上拿菜的方法.
放菜:因為一張桌子由多個廚師放菜,是以廚師放菜的要被同步,如果桌子上已經有十盤菜了.所有廚師
就要等待:
public synchronized void putFood(Food f){
while(this.size() >= this.maxSize){
try{
this.wait();
}catch(Exception e){}
}
this.add(f);
notifyAll();
}
拿菜:同上面,如果桌子上一盤菜也沒有,所有食客都要等待:
public synchronized Food getFood(){
while(this.size() <= 0){
try{
this.wait();
}catch(Exception e){}
}
Food f = (Food)this.removeFirst();
notifyAll();
return f;
}
廚師類:
由于多個廚師要往一張桌子上放菜,是以他們要操作的桌子應該是同一個對象,我們從構造
方法中将桌子對象傳進去以便控制在主線程中隻産生一張桌子.
廚師做菜要用一定的時候,我用在make方法中用sleep表示他要消耗和時候,用200加上200的随機數保
證時間有200-400ms中.做好後就要往桌子上放.
這裡有一個非常重要的問題一定要注意,就是對什麼範圍同步的問題,因為産生競争的是桌子,是以所
有putFood是同步的,而我們不能把廚師自己做菜的時間也放在同步中,因為做菜是各自做的.同樣食客
吃菜的時候也不應該同步,隻有從桌子中取菜的時候是競争的,而具體吃的時候是各自在吃.
是以廚師類的代碼如下:
class Chef extends Thread{
Table t;
Random r = new Random(12345);
public Chef(Table t){
this.t = t;
}
public void run(){
while(true){
Food f = make();
t.putFood(f);
}
}
private Food make(){
try{
Thread.sleep(200+r.nextInt(200));
}catch(Exception e){}
return new Food();
}
}
同理我們産生食客類的代碼如下:
class Eater extends Thread{
Table t;
Random r = new Random(54321);
public Eater(Table t){
this.t = t;
}
public void run(){
while(true){
Food f = t.getFood();
eat(f);
}
}
private void eat(Food f){
try{
Thread.sleep(400+r.nextInt(200));
}catch(Exception e){}
}
}
完整的程式在這兒:
package debug;
import java.util.regex.*;
import java.util.*;
class Food{}
class Table extends LinkedList{
int maxSize;
public Table(int maxSize){
this.maxSize = maxSize;
}
public synchronized void putFood(Food f){
while(this.size() >= this.maxSize){
try{
this.wait();
}catch(Exception e){}
}
this.add(f);
notifyAll();
}
public synchronized Food getFood(){
while(this.size() <= 0){
try{
this.wait();
}catch(Exception e){}
}
Food f = (Food)this.removeFirst();
notifyAll();
return f;
}
}
class Chef extends Thread{
Table t;
String name;
Random r = new Random(12345);
public Chef(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = make();
System.out.println(name+" put a Food:"+f);
t.putFood(f);
}
}
private Food make(){
try{
Thread.sleep(200+r.nextInt(200));
}catch(Exception e){}
return new Food();
}
}
class Eater extends Thread{
Table t;
String name;
Random r = new Random(54321);
public Eater(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = t.getFood();
System.out.println(name+" get a Food:"+f);
eat(f);
}
}
private void eat(Food f){
try{
Thread.sleep(400+r.nextInt(200));
}catch(Exception e){}
}
}
public class Test {
public static void main(String[] args) throws Exception{
Table t = new Table(10);
new Chef("Chef1",t).start();
new Chef("Chef2",t).start();
new Chef("Chef3",t).start();
new Chef("Chef4",t).start();
new Eater("Eater1",t).start();
new Eater("Eater2",t).start();
new Eater("Eater3",t).start();
new Eater("Eater4",t).start();
new Eater("Eater5",t).start();
new Eater("Eater6",t).start();
}
}
這一個例子中,我們主要關注以下幾個方面:
1.同步方法要保護的對象,本例中是保護桌子,不能同時往上放菜或同時取菜.
假如我們把putFood方法和getFood方法在廚師類和食客類中實作,那麼我們應該如此:
(以putFood為例)
class Chef extends Thread{
Table t;
String name;
public Chef(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = make();
System.out.println(name+" put a Food:"+f);
putFood(f);
}
}
private Food make(){
Random r = new Random(200);
try{
Thread.sleep(200+r.nextInt());
}catch(Exception e){}
return new Food();
}
public void putFood(Food f){//方法本身不能同步,因為它同步的是this.即Chef的執行個體
synchronized (t) {//要保護的是t
while (t.size() >= t.maxSize) {
try {
t.wait();
}
catch (Exception e) {}
}
t.add(f);
t.notifyAll();
}
}
}
2.同步的範圍,在本例中是放和取兩個方法,不能把做菜和吃菜這種各自不相幹的工作
放在受保護的範圍中.
3.參與者與容積比.
對于生産者和消費者的比例,以及桌子所能放置最多菜的數量三者之間的關系
是影響性能的重要因素,如果是過多的生産者在等待,則要增加消費者或減少生産者的資料,反之
則增加生産者或減少消費者的數量.
另外如果桌子有足夠的容量可以很大程式提升性能,這種情況下可以同時提高生産者和
消費者的數量,但足夠大的容時往往你要有足夠大的實體記憶體.
從這個例子中還可以學習到,使用wait()而不是sleep()因為 wati是釋放目前的鎖,進入等待狀态,而sleep()是不釋放目前的鎖,進入等待狀态,這種睡覺還要抱着PAD的自私行為是另别人十分厭惡的,是以沒有特殊情況,我們一般不會在同步方法内使用sleep()的。但是若是非同步方法想實作“等待”,用它就可以了,若是非要用wait也可以,必須把wait放到同步方法内,代碼: public static mySleep(long l){
Object o = new Object();
synchronized(o){
try{
o.wait(l);
}catch(Exception e){}
}
}
放心吧,沒有人能在這個方法外調用o.notify[All],是以o.wait(l)會一直等到設定的時間才會運作完成.
Thread線程對象隻能start()一次,即隻能建立一個線程。最重要的原因是(保證資料安全,實作資料共享) 簡單說:
class T extends Thread{
Object x;
public void run(){//......;}
}
T t = new T();
當T的執行個體t運作後,t所包含的資料x隻能被一個t.start();對象共享,除非聲明成
static Object x;
一個t的執行個體資料隻能被一個線程通路.意思是"一個資料執行個體對應一個線程".
而假如我們從外部傳入資料,比如
class T extends Thread{
private Object x;
public T(Object x){
this.x = x;
}
public void run(){//......;}
}
這樣我們就可以先生成一個x對象傳給多個Thread對象,多個線程共同操作一個資料.也就是"一個資料執行個體對應多個線程". 這個從外部傳遞進來的 x 就是我們的緊俏資源,我們要保護的對象。
現在我們把資料更好地組織一下,把要操作的資料Object x和要進行的操作一個封裝到Runnable的run()方法中,把Runnable執行個體從外部傳給多個Thread對象.這樣,我們就有了: [一個對象的多個線程]也就是線程池的概念。
是以我們總結下: 一個資料執行個體可以對應多個Thread線程對象産生的線程 一個對象執行個體(資料封裝在對象裡)可以對應一個Runnable對象産生的多個線程
再談Interrupt() 不客氣地說,至少有一半人認為,線程的"中斷"就是讓線程停止.
如果你也這麼認為,那你對多線程程式設計還沒有入門.
在java中,線程的中斷(interrupt)隻是改變了線程的中斷狀态,至于這個中斷狀态改變後
帶來的結果,那是無法确定的,有時它更是讓停止中的線程繼續執行的唯一手段.不但不是
讓線程停止運作,反而是繼續執行線程的手段.
對于執行一般邏輯的線程,如果調用調用它的interrupt()方法,那麼對這個線程沒有任何
影響,比如線程a正在執行:
while(條件) x ++;
這樣的語句,如果其它線程調用a.interrupt();那麼并不會影響a對象上運作的線程,如果
在其它線程裡測試a的中斷狀态它已經改變,但并不會停止這個線程的運作.
在一個線程對象上調用interrupt()方法,真正有影響的是wait,join,sleep方法,當然這三個
方法包括它們的重載方法.
請注意:[上面這三個方法都會抛出InterruptedException],記住這句話,下面我會重複.
一個線程在調用interrupt()後,自己不會抛出InterruptedException異常,是以你看到
interrupt()并沒有抛出這個異常,是以我上面說如果線程a正在執行while(條件) x ++;
你調用a.interrupt();後線程會繼續正常地執行下去.
但是,如果一個線程被調用了interrupt()後,它的狀态是已中斷的.這個狀态對于正在執行
wait,join,sleep的線程,卻改變了線程的運作結果.
一.對于wait中等待notify/notifyAll喚醒的線程,其實這個線程已經"暫停"執行,因為
它正在某一對象的休息室中,這時如果它的中斷狀态被改變,那麼它就會抛出異常.
這個InterruptedException異常不是線程抛出的,而是wait方法,也就是對象的wait方法内部
會不斷檢查在此對象上休息的線程的狀态,如果發現哪個線程的狀态被置為已中斷,則會抛出
InterruptedException,意思就是這個線程不能再等待了,其意義就等同于喚醒它了.
這裡唯一的差別是,被notify/All喚醒的線程會繼續執行wait下面的語句,而在wait
中被中斷的線程則将控制權交給了catch語句.一些正常的邏輯要被放到catch中來運作.
但有時這是唯一手段,比如一個線程a在某一對象b的wait中等待喚醒,其它線程必須
擷取到對象b的監視鎖才能調用b.notify()[All],否則你就無法喚醒線程a,但在任何線程中可
以無條件地調用a.interrupt();來達到這個目的.隻是喚醒後的邏輯你要放在catch中,當然同
notify/All一樣,繼續執行a線程的條件還是要等拿到b對象的監視鎖.
二.對于sleep中的線程,如果你調用了Thread.sleep(一年);現在你後悔了,想讓它早
些醒過來,調用interrupt()方法就是唯一手段,隻有改變它的中斷狀态,讓它從sleep中将控制
權轉到處理異常的catch語句中,然後再由catch中的處理轉換到正常的邏輯.同樣,地于join中
的線程你也可以這樣處理.
對于一般介紹多線程模式的書上,他們會這樣來介紹:當一個線程被中斷後,在進入
wait,sleep,join方法時會抛出異常.
是的,這一點也沒有錯,但是這有什麼意義呢?如果你知道那個線程的狀态已經處于中
斷狀态,為什麼還要讓它進入這三個方法呢?當然有時是必須這麼做的,但大多數時候沒有這麼
做的理由,是以我上面主要介紹了在已經調用這三個方法的線程上調用interrupt()方法讓它從
這幾個方法的"暫停"狀态中恢複過來.這個恢複過來就可以包含兩個目的:
一.[可以使線程繼續執行],那就是在catch語句中執行醒來後的邏輯,或由catch語句
轉回正常的邏輯.總之它是從wait,sleep,join的暫停狀态活過來了.
二.[可以直接停止線程的運作],當然在catch中什麼也不處理,或return,那麼就完成
了目前線程的使命,可以使在上面"暫停"的狀态中立即真正的"停止".
那麼如何正确的中斷線程?
中斷線程
有了上一節[線程的中斷],我們就好進行如何[中斷線程]了.這絕對不是玩一個文字遊戲.
是因為"線程的中斷"并不能保證"中斷線程",是以我要特别地分為兩節來說明.
這裡說的"中斷線程"意思是"停止線程",而為什麼不用"停止線程"這個說法呢?
因為線程有一個明确的stop方法,但它是反對使用的,是以請大家記住,在java中以後不要提
停止線程這個說法,忘記它!
但是,作為介紹線程知識的我,我仍然要告訴你為什麼不用"停止線程"的理由.
[停止線程]
當在一個線程對象上調用stop()方法時,這個線程對象所運作的線程就會立即停止,
并抛出特殊的ThreadDeath()異常.這裡的"立即"因為太"立即"了,就象一個正在擺弄自己的
玩具的孩子,聽到大人說快去睡覺去,就放着滿地的玩具立即睡覺去了.這樣的孩子是不乖的.
假如一個線程正在執行:
synchronized void {
x = 3;
y = 4;
}
由于方法是同步的,多個線程通路時總能保證x,y被同時指派,而如果一個線程正在執行到
x = 3;時,被調用了 stop()方法,即使在同步塊中,它也幹脆地stop了,這樣就産生了不完整
的殘廢資料.而多線程程式設計中最最基礎的條件要保證資料的完整性,是以請忘記線程的stop
方法,以後我們再也不要說"停止線程"了.
如何才能"結束"一個線程?
[中斷線程]
結束一個線程,我們要分析線程的運作情況.也就是線程正在幹什麼.如果那個孩子
什麼事也沒幹,那就讓他立即去睡覺.而如果那個孩子正在擺弄他的玩具,我們就要讓它把玩
具收拾好再睡覺.
是以一個線程從運作到真正的結束,應該有三個階段:
1.正常運作.
2.處理結束前的工作,也就是準備結束.
3.結束退出.
在我的JDBC專欄中我N次提醒在一個SQL邏輯結束後,無論如何要保證關閉Connnection
那就是在finally從句中進行.同樣,線程在結束前的工作應該在finally中來保證線程退出前
一定執行:
try{
正在邏輯
}catch(){}
finally{
清理工作
}
那麼如何讓一個線程結束呢?既然不能調用stop,可用的隻的interrupt()方法.但interrupt()
方法隻是改變了線程的運作狀态,如何讓它退出運作?
對于一般邏輯,隻要線程狀态為已經中斷,我們就可以讓它退出,是以這樣的語句可以保證
線程在中斷後就能結束運作:
while(!isInterrupted()){
正常邏輯
}
這樣如果這個線程被調用interrupt()方法,isInterrupted()為true,就會退出運作.但是
如果線程正在執行wait,sleep,join方法,你調用interrupt()方法,這個邏輯就不完全了.
如果一個有經驗的程式員來處理線程的運作的結束:
public void run(){
try{
while(!isInterrupted()){
正常工作
}
}
catch(Exception e){
return;
}
finally{
清理工作
}
}
我們看到,如果線程執行一般邏輯在調用innterrupt後.isInterrupted()為true,退出循環後執行
清理工作後結束,即使線程正在wait,sleep,join,也會抛出異常執行清理工作後退出.
這看起來非常好,線程完全按最我們設定的思路在工作.但是,并不是每個程式員都有這種認識,如果
他聰明的自己處理異常會如何?事實上很多或大多數程式員會這樣處理:
public void run(){
while(!isInterrupted()){
try{
正常工作
}catch(Exception e){
//nothing
}
finally{
}
}
}
想一想,如果一個正在sleep的線程,在調用interrupt後,會如何?
wait方法檢查到isInterrupted()為true,抛出異常,而你又沒有處理.而一個抛出了
InterruptedException的線程的狀态馬上就會被置為非中斷狀态,如果catch語句沒有處理異常,則
下一次循環中isInterrupted()為false,線程會繼續執行,可能你N次抛出異常,也無法讓線程停止.
那麼如何能確定線程真正停止?
線上程同步的時候我們有一個叫"二次惰性檢測"(double check),能在提高效率的基礎上又
確定線程真正中同步控制中.
那麼我把線程正确退出的方法稱為"雙重安全退出",即不以isInterrupted()為循環條件.而
以一個标記作為循環條件:
class MyThread extend Thread{
private boolean isInterrupted = false;//這一句以後要修改
public void interrupt(){
isInterrupted = true;
super.interrupt();
}
public void run(){
while(!isInterrupted){
try{
正常工作
}catch(Exception e){
//nothing
}
finally{
}
}
}
}
試試這段程式,可以正确工作嗎?