本文將介紹:
- Java線程基本操作(創建、等待等)
- Java線程同步原語(同步、互斥)
Java線程基本操作
Java的線程API以java.lang.Thread類提供,線程的基本操作被封裝爲爲Thread類的方法,其中常用的方法是:
方法 | 說明 | |
void | start() | 啓動線程 |
void | join() | 等待線程結束 |
創建(啓動)線程
Java中,創建線程的過程分爲兩步:
- 創建可執行(Runnable)的線程對象;
- 調用它的start()方法;
- 繼承(extends)Thread類,重寫(override)run()方法;
- 實現(implements)Runnable接口(實現run()方法);
兩種創建線程的對象的代碼實例如下:
繼承Thread類
繼承Thread類創建線程,如下:
class ExtendsThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; ++i) {
System.out.print("*");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestExtendsThread {
public static void main(String[] args) {
// 1.創建線程對象
Thread backThread = new ExtendsThread();
// 2.啓動線程
backThread.start();
for(int i=0; i < 100; ++i) {
System.out.print("#");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
該程序打印出的*和#是交替的;這說明backThread的run()和主線程同時在執行!當然,如果一個線程的代碼不是多次重複使用,可以將該線程寫成“匿名內部類”的形式:public class TestExtendsThread {
public static void main(String[] args) {
new Thread() {
public void run() {
for (int i = 0; i < 100; ++i) {
System.out.print("*");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
for (int i = 0; i < 100; ++i) {
System.out.print("#");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
實現Runnable接口
Java中創建線程對象的另一種方法是:實現Runnable接口,再用具體類的實例作爲Thread的參數構造線程,代碼如下:
class RunnableImpl implements Runnable {
@Override
public void run() {
for(int i=0; i < 100; ++i) {
System.out.print("*");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestImlementsRunnable {
public static void main(String[] args) {
Runnable callback = new RunnableImpl();
Thread backThread = new Thread(callback);
backThread.start(); // 啓動線程
for(int i=0; i < 100; ++i) {
System.out.print("#");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
類似地,RunnableImpl若是不被複用,也可寫成“匿名內部類”的形式:public class TestImlementsRunnable {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i < 100; ++i) {
System.out.print("*");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
for(int i=0; i < 100; ++i) {
System.out.print("#");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
這兩種方法都實現了run()方法,而Thread的start()方法會調用傳入的Runnable對象的run()方法(或是調用自己的run方法)。 run()在這裏的作用就是爲新線程提供一個入口,或者說run描述了新線程將來要“幹什麼”;相當於一些C庫的回調函數。
等待線程結束
Thread的join()方法提供了“等待線程結束”的功能,Java的主線程默認會等待其他線程的結束。Thread.join()提供的是:一個線程等待另一個線程的功能;例如,在main方法(主線程)中調用 backThread.join();則主線程將會在調用處等待,直到backThread執行完畢。如下代碼是典型的start和join的使用順序:
// in main()
Runnable r = new Runnable() {
public void run() {
// ...
}
};
Thread back = new Thread(r);
back.start();
back.join();
這段代碼對應的序列圖如下:start()的作用是啓動一個線程(程序執行流),使得調用處的執行流程一分爲二;而join()的作用則與start相反,使得兩個執行流程“合二爲一”,如下圖所示:
兩個線程和幾個方法執行時間的先後關係,執行流程先“一分爲二”和“合二爲一”。
互斥
Java的互斥語義由synchronized關鍵字提供,具體有兩種:
- synchronized代碼塊
- synchronized方法
下面分別介紹。
爲什麼需要互斥?
由於本文的定位爲多線程編程入門,所以順便介紹一下爲什麼會有互斥問題。
猜測下面的程序的輸出:
public class NonAtomic {
static int count = 0;
public static void main(String[] args) {
Thread back = new Thread() {
@Override
public void run() {
for(int i=0; i<10000; ++i) {
++count;
}
}
};
back.start();
for(int i=0; i<10000; ++i) {
++count;
}
try {
back.join(); // wait for back thread finish.
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
這個程序並不能像想象中的那樣輸出20000,而總是小了一些。爲什麼會這樣?因爲++count;操作並不是“原子性”的,即不是一條指令就能完成的功能。在多數體系結構上,實現內存中的整數“自增”操作至少需要三步:
- 從內存中讀數據到寄存器
- 在寄存器內加一
- 寫回內存
在這幅圖中,A、B兩個線程同時對value執行“自增”,預期的value值應該是11,而實際的value值卻是10。
由此可見,要保證多線程環境下“自增”操作的正確性,就必須保證以上三個操作“一次性執行”而不被其他線程干擾,這就是所謂的“原子性”。
synchronized代碼塊
synchronized代碼塊的形式如下:
synchronized(obj)
{
// do something.
}
這段代碼保證了花括號內代碼的“原子性”,就是說兩個線程同時執行這一代碼塊的時候會表現出“要麼都不執行,要麼全部執行”的特性,即“互斥執行”。兩個使用同一obj的synchronized代碼塊也同樣具有“互斥執行”的特性。只需將上面的NonAtomic稍作修改:
// static int count = 0; 後加一行:
static Object lock = new Object();
// ++count改爲:
synchronized(lock) {
++count;
}
就能保證程序的輸出爲20000。
synchronized方法
synchronized代碼塊通常是方法內的一部分,如果整個方法體都需要用synchronized(this)鎖定,那麼也可以用synchronized關鍵字修飾這個方法。
就是說,這個方法:
public synchronized void someMethod() {
// do something...
}
等價於: public void someMethod() {
synchronized(this) {
// do something...
}
}
同步
通俗地說,“同步”就是保證兩個線程事件的時序(先後)關係,這在多線程環境下非常有用。例如,兩個線程A, B正在執行一系列工作Ai, Bi,現在想要使得A3發生在B2之後,就需要使用“同步原語”:
支持“同步”操作的調用叫做“同步原語”,在多數《操作系統》教材中,這種原語通常被定義爲條件變量(condition variable)。
Java的同步原語爲java.lang.Object類的幾個方法:
- wait() 等待通知,該調用會阻塞當前線程。
- notify() 發出通知,如果有多個線程阻塞在該obj上,該調用會喚醒一個(阻塞)等待該obj的線程。
- notifyAll()發出通知,如果有多個線程阻塞在該obj上,該調用會喚醒所有(阻塞)等待該obj的線程。
notifyAll()通常用於通知“狀態改變”,例如,一個多線程測試程序中,多個後臺線程被創建後,全都等待主線程發出“開始測試”的命令,此時主線程可用notifyAll()通知各個測試線程。
例如如下代碼,模擬運動員起跑過程:首先,發令員等待個運動員就緒;然後發令員一聲槍響,所有運動員起跑;
public class TestStartRunning {
static final int NUM_ATHLETES = 10;
static int readyCount = 0;
static Object ready = new Object();
static Object start = new Object();
public static void main(String[] args) {
Thread[] athletes = new Thread[NUM_ATHLETES];
// 創建運動員
for (int i = 0; i < athletes.length; ++i) {
final int num = i;
athletes[i] = new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " ready!");
synchronized (ready) {
++readyCount;
ready.notify(); // 通知發令員,“I'm ready!”
}
// 等待發令槍響
try {
synchronized (start) {
start.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " go!");
}
};
}
// 運動員上場
for (int i = 0; i < athletes.length; ++i)
athletes[i].start();
// 主線程充當裁判員角色
try {
synchronized (ready) {
// 等待所有運動員就位
while (readyCount < athletes.length) {
ready.wait();
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " START!");
synchronized (start) {
start.notifyAll(); // 打響發令槍
}
}
}
信號丟失
wait/notify/notifyAll提供了一種線程間事件通知的方式,但這種通知並不能被有效的“記住”;所以,就存在通知丟失(notify missing)的可能——發出通知的線程先notify,接收通知的線程後wait,此時這個事先發出的通知就會丟失。在POSIX規範上,叫做信號丟失;由於現在的多數操作系統(LINUX,Mac,Unix)都遵循POSIX;所以“信號丟失”這個詞使用的更廣泛。
如下是一個演示通知丟失的代碼:
public class TestNotifyMissing {
static Object cond = new Object();
public static void main(String[] args) {
new Thread() {
public void run() {
try {
Thread.sleep(1000);
System.out.println("[back] wait for notify...");
synchronized (cond) {
cond.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[back] wakeup");
}
}.start();
System.out.println("[main] notify");
synchronized (cond) {
cond.notify();
}
}
}
這個程序不能正常退出,後臺線程因爲錯過了主線程發出的通知而一直在後臺等待,程序也不會輸出“[back] wake up”。
通俗地說,wait/notify只是一種口頭交流,如果你沒有聽到,就會錯過(而不像郵件、公告板,你收到通知的時間可以比別人發出的時間晚)。
如何避免通知丟失呢?由於notify本身不具備“記憶”,所以可以使用額外的變量作爲“公告板”;在notify之前修改這個“公告板”;這樣,即便其他線程調用wait的時間晚於notify的時間,也能看到寫在“公共板”上的通知。
這同時也解釋了另外一個語言設計上的問題:爲什麼Java的wait和notify端都必須要用synchronized鎖定?首先,這不是語法級別的規定,不這麼寫也能編譯通過,只是運行時會拋異常;這是JVM的一種運行時安全檢查機制,這種機制是在提醒我們——應該使用額外的變量來防止產生通知丟失。例如剛纔的NotifyMissing只需稍作修改就能夠正常結束
public class TestNotifyMissingSolution {
static boolean notified = false; // +++++
static Object cond = new Object();
public static void main(String[] args) {
new Thread() {
public void run() {
try {
Thread.sleep(1000);
System.out.println("[back] wait for notify...");
synchronized (cond) {
while(!notified) // +++++
cond.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[back] wakeup");
}
}.start();
System.out.println("[main] notify");
synchronized (cond) {
notified = true; // +++++
cond.notify();
}
System.out.println("[main] notified");
}
}
虛假喚醒
在例子TestNotifyMissingSolution中,cond.wait()前添加if(!notified),也能夠正常運行;但這種做法與文檔中給出的while(...)不同,文檔中同時指出了虛假喚醒(Spurious Wakeup)的概念。虛假喚醒在《Programming with POSIX Threads》中的解釋是::當一個線程wait在某個條件變量上,這個條件變量上沒發生broadcast(相當於notifyAll)或signal(相當於notify)調用,wait也又可能返回。虛假喚醒聽起來很奇怪,但是在多核系統上,使條件喚醒完全可預測可能導致多數條件變量操作變慢。"
爲了防止虛假喚醒,需要在wait返回後繼續檢查某個條件是否達成,所有通常wait端的條件寫爲while而不是if,在Java中通常是:
// 等待線程:
synchronized(cond) {
while(!done) {
cond.wait();
}
}
// 喚醒線程:
doSth();
synchronized(cond) {
done = true;
cond.notify();
}
總結
在<操作系統>的概念中,提供“互斥語義”的叫互斥器(Mutex),提供同步語義的叫條件變量(Condition Variable)。而在Java中,synchronized關鍵字和java.lang.Object提供了互斥量(mutex)語義,java.lang.Object的wait/notify/notifyAll則提供了條件變量語義。
另外,多線程環境下對象的回收是十分困難的,Java運行環境的垃圾回收(Garbage Collection,GC)功能減輕了程序員的負擔。
參考
Java 1.6 apidocs Thread,http://tool.oschina.net/uploads/apidocs/jdk-zh/java/lang/Thread.html
《Java Concurrency in Practice》(中譯本名爲《Java併發實踐》)
Spurious Wakeup -- Wikipedia,http://en.wikipedia.org/wiki/Spurious_wakeup
多線程編程中條件變量和虛假喚醒(spurious wakeup)的討論,http://siwind.iteye.com/blog/1469216