一、Java線程:概念與原理
1、作業系統中線程和進程的概念
現在的作業系統是多任務作業系統。多線程是實現多任務的一種方式。進程是指一個記憶體中啟動並執行應用程式,每個進程都有自己獨立的一塊記憶體空間,一個進程中可以啟動多個線程。比如在Windows系統中,一個啟動並執行exe就是一個進程。線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬於某個進程,進程中的多個線程共用進程的記憶體。“同時”執行是人的感覺,線上程之間實際上輪換執行。
2、Java中的線程
在Java中,“線程”指兩件不同的事情:
1、java.lang.Thread類的一個執行個體;
2、線程的執行。
使用java.lang.Thread類或者java.lang.Runnable介面編寫代碼來定義、執行個體化和啟動新線程。
一個Thread類執行個體只是一個對象,像Java中的任何其他對象一樣,具有變數和方法,生死於堆上。
Java中,每個線程都有一個調用棧,即使不在程式中建立任何新的線程,線程也在後台運行著。
一個Java應用總是從main()方法開始運行,mian()方法運行在一個線程內,它被稱為主線程。
一旦建立一個新的線程,就產生一個新的調用棧。
線程總體分兩類:使用者線程和守候線程。當所有使用者線程執行完畢的時候,JVM自動關閉。但是守候線程卻不獨立於JVM,守候線程一般是由作業系統或者使用者自己建立的。
二、java線程:建立與啟動
a、定義線程
1、擴充java.lang.Thread類。
此類中有個run()方法,應該注意其用法:
public void run()
如果該線程是使用獨立的 Runnable 運行物件建構的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。
Thread 的子類應該重寫該方法。
2、實現java.lang.Runnable介面。
void run()
使用實現介面 Runnable 的對象建立一個線程時,啟動該線程將導致在獨立執行的線程中調用對象的 run 方法。
方法 run 的常規協定是,它可能執行任何所需的操作。
b、執行個體化線程
1、如果是擴充java.lang.Thread類的線程,則直接new即可。
2、如果是實現了java.lang.Runnable介面的類,則用Thread的構造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
c、啟動線程
線上程的Thread對象上調用start()方法,而不是run()或者別的方法。
在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
在調用start()方法之後:發生了一系列複雜的事情
啟動新的執行線程(具有新的調用棧);
該線程從新狀態轉移到可運行狀態;
當該線程獲得機會執行時,其目標run()方法將運行。
注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的。但並不啟動新的線程。
四、例子
1、實現Runnable介面的多線程例子
/**
* 實現Runnable介面的類
*
* @author leizhimin 2008-9-13 18:12:10
*/
public class DoSomething implements Runnable {
private String name;
public DoSomething(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
for (long k = 0; k < 100000000; k++) ;
System.out.println(name + ": " + i);
}
}
}
/**
* 測試Runnable類實現的多線程程式
*
* @author leizhimin 2008-9-13 18:15:02
*/
public class TestRunnable {
public static void main(String[] args) {
DoSomething ds1 = new DoSomething("阿三");
DoSomething ds2 = new DoSomething("李四");
Thread t1 = new Thread(ds1);
Thread t2 = new Thread(ds2);
t1.start();
t2.start();
}
}
執行結果:
李四: 0
阿三: 0
李四: 1
阿三: 1
李四: 2
李四: 3
阿三: 2
李四: 4
阿三: 3
阿三: 4
Process finished with exit code 0
2、擴充Thread類實現的多線程例子
/**
* 測試擴充Thread類實現的多線程程式
*
* @author leizhimin 2008-9-13 18:22:13
*/
public class TestThread extends Thread{
public TestThread(String name) {
super(name);
}
public void run() {
for(int i = 0;i<5;i++){
for(long k= 0; k <100000000;k++);
System.out.println(this.getName()+" :"+i);
}
}
public static void main(String[] args) {
Thread t1 = new TestThread("阿三");
Thread t2 = new TestThread("李四");
t1.start();
t2.start();
}
}
執行結果:
阿三 :0
李四 :0
阿三 :1
李四 :1
阿三 :2
李四 :2
阿三 :3
阿三 :4
李四 :3
李四 :4
Process finished with exit code 0
對於上面的多線程程式碼來說,輸出的結果是不確定的。其中的一條語句for(long k= 0; k <100000000;k++);是用來類比一個非常耗時的操作的。
五、一些常見問題
1、線程的名字,一個運行中的線程總是有名字的,名字有兩個來源,一個是虛擬機器自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛擬機器總會為線程指定名字,並且主線程的名字總是mian,非主線程的名字不確定。
2、線程都可以設定名字,也可以擷取線程的名字,連主線程也不例外。
3、擷取當前線程的對象的方法是:Thread.currentThread();
4、在上面的代碼中,只能保證:每個線程都將啟動,每個線程都將運行直到完成。一系列線程以某種順序啟動並不意味著將按該順序執行。對於任何一組啟動的線程來說,發送器不能保證其執行次序,期間也無法保證。
5、當線程目標run()方法結束時該線程完成。
6、一旦線程啟動,它就永遠不能再重新啟動。只有一個新的線程可以被啟動,並且只能一次。一個可啟動並執行線程或死線程可以被重新啟動。
7、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程發送器決定實際運行哪個處於可運行狀態的線程。
眾多可運行線程中的某一個會被選中做為當前線程。可運行線程被選擇啟動並執行順序是沒有保障的。
8、儘管通常採用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成“一輪”時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端為止,它才能被再次選中。事實上,我們把它稱為可運行池而不是一個可運行隊列,目的是協助認識線程並不都是以某種有保障的順序排列唱呢個一個隊列的事實。
9、儘管我們沒有無法控制線程發送器,但可以通過別的方式來影響線程調度的方式。
三、java線程:互動
線程互動是比較複雜的問題,給定一個情境,編寫代碼來恰當使用等待、通知和通知所有線程。
一、線程互動的基礎知識
線程互動知識點需要從java.lang.Object的類的三個方法來學習:
void notify()
喚醒在此對象監視器上等待的單個線程。
void notifyAll()
喚醒在此對象監視器上等待的所有線程。
void wait()
導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法。
當然,wait()還有另外兩個重載方法:
void wait(long timeout)
導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
void wait(long timeout, int nanos)
導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。
以上這些方法是協助線程傳遞線程關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。
wait()、notify()、notifyAll()都是Object的執行個體方法。與每個對象具有鎖一樣,每個對象可以有一個線程列表,他們等待來自該訊號(通知)。線程通過執行對象上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到調用對象的notify()方法為止。如果多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。如果沒有線程等待,則不採取任何特殊操作。
下面看個例子就明白了:
/**
* 計算輸出其他線程鎖計算的資料
*
* @author leizhimin 2008-9-15 13:20:38
*/
public class ThreadA {
public static void main(String[] args) {
ThreadB b = new ThreadB();
//啟動計算線程
b.start();
//線程A擁有b對象上的鎖。線程為了調用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (b) {
try {
System.out.println("等待對象b完成計算。。。");
//當前線程A等待
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b對象計算的總和是:" + b.total);
}
}
}
/**
* 計算1+2+3 ... +100的和
*
* @author leizhimin 2008-9-15 13:20:49
*/
public class ThreadB extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
//(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程A被喚醒
notify();
}
}
}
等待對象b完成計算。。。
b對象計算的總和是:5050
Process finished with exit code 0
千萬注意:
當在對象上調用wait()方法時,執行該代碼的線程立即放棄它在對象上的鎖。然而調用notify()時,並不意味著這時線程會放棄其鎖。如果線程榮然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,只要調用notify()並不意味著這時該鎖變得可用。
二、多個線程在等待一個對象鎖時候使用notifyAll()
在多數情況下,最好通知等待某個對象的所有線程。如果這樣做,可以在對象上使用notifyAll()讓所有在此對象上等待的線程衝出等待區,返回到可運行狀態。