1. 程式人生 > >七. 多線程編程8.線程同步

七. 多線程編程8.線程同步

幸運 實例 語句 情況 獲得 限制 不同 共享 所有

當兩個或兩個以上的線程需要共享資源,它們需要某種方法來確定資源在某一刻僅被一個線程占用。達到此目的的過程叫做同步(synchronization)。像你所看到的,Java為此提供了獨特的,語言水平上的支持。

同步的關鍵是管程(也叫信號量semaphore)的概念。管程是一個互斥獨占鎖定的對象,或稱互斥體(mutex)。在給定的時間,僅有一個線程可以獲得管程。當一個線程需要鎖定,它必須進入管程。所有其他的試圖進入已經鎖定的管程的線程必須掛起直到第一個線程退出管程。這些其他的線程被稱為等待管程。一個擁有管程的線程如果願意的話可以再次進入相同的管程。

如果你用其他語言例如C或C++時用到過同步,你會知道它用起來有一點詭異。這是因為很多語言它們自己不支持同步。相反,對同步線程,程序必須利用操作系統源語。幸運的是Java通過語言元素實現同步,大多數的與同步相關的復雜性都被消除。

你可以用兩種方法同步化代碼。兩者都包括synchronized關鍵字的運用,下面分別說明這兩種方法。

使用同步方法

Java中同步是簡單的,因為所有對象都有它們與之對應的隱式管程。進入某一對象的管程,就是調用被synchronized關鍵字修飾的方法。當一個線程在一個同步方法內部,所有試圖調用該方法(或其他同步方法)的同實例的其他線程必須等待。為了退出管程,並放棄對對象的控制權給其他等待的線程,擁有管程的線程僅需從同步方法中返回。

為理解同步的必要性,讓我們從一個應該使用同步卻沒有用的簡單例子開始。下面的程序有三個簡單類。首先是Callme,它有一個簡單的方法call( )。call( )方法有一個名為msg的String參數。該方法試圖在方括號內打印msg 字符串。有趣的事是在調用call( ) 打印左括號和msg字符串後,調用Thread.sleep(1000),該方法使當前線程暫停1秒。

下一個類的構造函數Caller,引用了Callme的一個實例以及一個String,它們被分別存在target 和 msg 中。構造函數也創建了一個調用該對象的run( )方法的新線程。該線程立即啟動。Caller類的run( )方法通過參數msg字符串調用Callme實例target的call( ) 方法。最後,Synch類由創建Callme的一個簡單實例和Caller的三個具有不同消息字符串的實例開始。

Callme的同一實例傳給每個Caller實例。
// This program is not synchronized.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}

class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
public void run() {
target.call(msg);
}
}

class Synch {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}

該程序的輸出如下:
Hello[Synchronized[World]
]
]

在本例中,通過調用sleep( ),call( )方法允許執行轉換到另一個線程。該結果是三個消息字符串的混合輸出。該程序中,沒有阻止三個線程同時調用同一對象的同一方法的方法存在。這是一種競爭,因為三個線程爭著完成方法。例題用sleep( )使該影響重復和明顯。在大多數情況,競爭是更為復雜和不可預知的,因為你不能確定何時上下文轉換會發生。這使程序時而運行正常時而出錯。

為達到上例所想達到的目的,必須有權連續的使用call( )。也就是說,在某一時刻,必須限制只有一個線程可以支配它。為此,你只需在call( ) 定義前加上關鍵字synchronized,如下:
class Callme {
synchronized void call(String msg) {
...

這防止了在一個線程使用call( )時其他線程進入call( )。在synchronized加到call( )前面以後,程序輸出如下:
[Hello]
[Synchronized]
[World]

任何時候在多線程情況下,你有一個方法或多個方法操縱對象的內部狀態,都必須用synchronized 關鍵字來防止狀態出現競爭。記住,一旦線程進入實例的同步方法,沒有其他線程可以進入相同實例的同步方法。然而,該實例的其他不同步方法卻仍然可以被調用。

同步語句

盡管在創建的類的內部創建同步方法是獲得同步的簡單和有效的方法,但它並非在任何時候都有效。這其中的原因,請跟著思考。假設你想獲得不為多線程訪問設計的類對象的同步訪問,也就是,該類沒有用到synchronized方法。而且,該類不是你自己,而是第三方創建的,你不能獲得它的源代碼。這樣,你不能在相關方法前加synchronized修飾符。怎樣才能使該類的一個對象同步化呢?很幸運,解決方法很簡單:你只需將對這個類定義的方法的調用放入一個synchronized塊內就可以了。

下面是synchronized語句的普通形式:
synchronized(object) {
// statements to be synchronized
}

其中,object是被同步對象的引用。如果你想要同步的只是一個語句,那麽不需要花括號。一個同步塊確保對object成員方法的調用僅在當前線程成功進入object管程後發生。

下面是前面程序的修改版本,在run( )方法內用了同步塊:
// This program uses a synchronized block.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e quqiongyule2.com ) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}

class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}

// synchronize calls to call()
public void run() {
synchronized(target) { // synchronized block
target.call(msg);
}
}
}

class Synch1 {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");

// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}

這裏,call( )方法沒有被synchronized修飾。而synchronized是在Caller類的run( )方法中聲明的。這可以得到上例中同樣正確的結果,因為每個線程運行前都等待先前的一個線程結束。

七. 多線程編程8.線程同步