線程的同步與共享
前面程序中的線程都是獨立的、異步執行的線程。但在很多情況下,多個線程需要共享數據資源,這就涉及到線程的同步與資源共享的問題。
1 資源沖突
下面的例子說明,多個線程共享資源,如果不加以控制可能會產生沖突。
程序CounterTest.java
class Num
{
private int x = 0;
private int y = 0;
void increase()
{
x++;
y++;
}
void testEqual()
{
System.out.println(x + "," + y + ":" + (x == y));
}
}
class Counter extends Thread
{
private Num num;
Counter(Num num)
{
this.num = num;
}
public void run()
{
while (true)
{
num.increase();
}
}
}
public class CounterTest
{
public static void main(String[] args)
{
Num num = new Num();
Thread count1 = new Counter(num);
Thread count2 = new Counter(num);
count1.start();
count2.start();
for (int i = 0; i < 100; i++)
{
num.testEqual();
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
}
}
}
}
上述程序在CounterTest類的main()方法中創建了兩個線程類Counter的對象count1和count2,這兩個對象共享一個Num類的對象num。兩個線程對象開始運行后,都調用同一個對象num的increase()方法來增加num對象的x和y的值。在main()方法的for()循環中輸出num對象的x和y的值。程序輸出結果有些x、y的值相等,大部分x、y的值不相等。
出現上述情況的原因是:兩個線程對象同時操作一個num對象的同一段代碼,通常將這段代碼段稱為臨界區(critical sections)。在線程執行時,可能一個線程執行了x++語句而尚未執行y++語句時,系統調度另一個線程對象執行x++和y++,這時在主線程中調用testEqual()方法輸出x、y的值不相等
2 對象鎖的實現
上述程序的運行結果說明了多個線程訪問同一個對象出現了沖突,為了保證運行結果正確(x、y的值總相等),可以使用Java語言的synchronized關鍵字,用該關鍵字修飾方法。用synchronized關鍵字修飾的方法稱為同步方法,Java平臺為每個具有synchronized代碼段的對象關聯一個對象鎖(object lock)。這樣任何線程在訪問對象的同步方法時,首先必須獲得對象鎖,然后才能進入synchronized方法,這時其他線程就不能再同時訪問該對象的同步方法了(包括其他的同步方法)。
通常有兩種方法實現對象鎖:
(1) 在方法的聲明中使用synchronized關鍵字,表明該方法為同步方法。
對于上面的程序我們可以在定義Num類的increase()和testEqual()方法時,在它們前面加上synchronized關鍵字,如下所示:
synchronized void increase(){
x++;
y++;
}
synchronized void testEqual(){
System.out.println(x+","+y+":"+(x==y)+":"+(x<y));
}
一個方法使用synchronized關鍵字修飾后,當一個線程調用該方法時,必須先獲得對象鎖,只有在獲得對象鎖以后才能進入synchronized方法。一個時刻對象鎖只能被一個線程持有。如果對象鎖正在被一個線程持有,其他線程就不能獲得該對象鎖,其他線程就必須等待持有該對象鎖的線程釋放鎖。
如果類的方法使用了synchronized關鍵字修飾,則稱該類對象是線程安全的,否則是線程不安全的。
如果只為increase()方法添加synchronized 關鍵字,結果還會出現x、y的值不相等的情況.
(2) 前面實現對象鎖是在方法前加上synchronized 關鍵字,這對于我們自己定義的類很容易實現,但如果使用類庫中的類或別人定義的類在調用一個沒有使用synchronized關鍵字修飾的方法時,又要獲得對象鎖,可以使用下面的格式:
synchronized(object){
//方法調用
}
假如Num類的increase()方法沒有使用synchronized 關鍵字,我們在定義Counter類的run()方法時可以按如下方法使用synchronized為部分代碼加鎖。
public void run(){
while(true){
synchronized (num){
num.increase();
}
}
}
同時在main()方法中調用testEqual()方法也用synchronized關鍵字修飾,這樣得到的結果相同。
synchronized(num){
num.testEqual();
}
對象鎖的獲得和釋放是由Java運行時系統自動完成的。
每個類也可以有類鎖。類鎖控制對類的synchronized static代碼的訪問。請看下面的例子:
public class X{
static int x, y;
static synchronized void foo(){
x++;
y++;
}
}
當foo()方法被調用時,調用線程必須獲得X類的類鎖。
3 線程間的同步控制
在多線程的程序中,除了要防止資源沖突外,有時還要保證線程的同步。下面通過生產者-消費者模型來說明線程的同步與資源共享的問題。
假設有一個生產者(Producer),一個消費者(Consumer)。生產者產生0~9的整數,將它們存儲在倉庫(CubbyHole)的對象中并打印出這些數來;消費者從倉庫中取出這些整數并將其也打印出來。同時要求生產者產生一個數字,消費者取得一個數字,這就涉及到兩個線程的同步問題。
這個問題就可以通過兩個線程實現生產者和消費者,它們共享CubbyHole一個對象。如果不加控制就得不到預期的結果。
1. 不同步的設計
首先我們設計用于存儲數據的類,該類的定義如下:
程序 CubbyHole.java
class CubbyHole{
private int content ;
public synchronized void put(int value){
content = value;
}
public synchronized int get(){
return content ;
}
}
_____________________________________________________________________________▃
CubbyHole類使用一個私有成員變量content用來存放整數,put()方法和get()方法用來設置變量content的值。CubbyHole對象為共享資源,所以用synchronized關鍵字修飾。當put()方法或get()方法被調用時,線程即獲得了對象鎖,從而可以避免資源沖突。
這樣當Producer對象調用put()方法是,它鎖定了該對象,Consumer對象就不能調用get()方法。當put()方法返回時,Producer對象釋放了CubbyHole的鎖。類似地,當Consumer對象調用CubbyHole的get()方法時,它也鎖定該對象,防止Producer對象調用put()方法。
接下來我們看Producer和Consumer的定義,這兩個類的定義如下:
程序 Producer.java
public class Producer extends Thread {
private CubbyHole cubbyhole;
private int number;
public Producer(CubbyHole c, int number) {
cubbyhole = c;
this.number = number;
}
public void run() {
for (int i = 0; i < 10; i++) {
cubbyhole.put(i);
System.out.println("Producer #" + this.number + " put: " + i);
try {
sleep((int)(Math.random() * 100));
} catch (InterruptedException e) { }
}
}
}
_____________________________________________________________________________▃
Producer類中定義了一個CubbyHole類型的成員變量cubbyhole,它用來存儲產生的整數,另一個成員變量number用來記錄線程號。這兩個變量通過構造方法傳遞得到。在該類的run()方法中,通過一個循環產生10個整數,每次產生一個整數,調用cubbyhole對象的put()方法將其存入該對象中,同時輸出該數。
下面是Consumer類的定義:
程序 Consumer.java
public class Consumer extends Thread {
private CubbyHole cubbyhole;
private int number;
public Consumer(CubbyHole c, int number) {
cubbyhole = c;
this.number = number;
}
public void run() {
int value = 0;
for (int i = 0; i < 10; i++) {
value = cubbyhole.get();
System.out.println("Consumer #" + this.number + " got: " + value);
}
}
}
_____________________________________________________________________________▃
在Consumer類的run()方法中也是一個循環,每次調用cubbyhole的get()方法返回當前存儲的整數,然后輸出。
下面是主程序,在該程序的main()方法中創建一個CubbyHole對象c,一個Producer對象p1,一個Consumer對象c1,然后啟動兩個線程。
程序 ProducerConsumerTest.java
public class ProducerConsumerTest {
public static void main(String[] args) {
CubbyHole c = new CubbyHole();
Producer p1 = new Producer(c, 1);
Consumer c1 = new Consumer(c, 1);
p1.start();
c1.start();
}
}
_____________________________________________________________________________▃
該程序中對CubbyHole類的設計,盡管使用了synchronized關鍵字實現了對象鎖,但這還不夠。程序運行可能出現下面兩種情況:
如果生產者的速度比消費者快,那么在消費者來不及取前一個數據之前,生產者又產生了新的數據,于是消費者很可能會跳過前一個數據,這樣就會產生下面的結果:
Consumer: 3
Producer: 4
Producer: 5
Consumer: 5
…
反之,如果消費者比生產者快,消費者可能兩次取同一個數據,可能產生下面的結果:
Producer: 4
Consumer: 4
Consumer: 4
Producer: 5
…
2. 監視器模型
為了避免上述情況發生,就必須使生產者線程向CubbyHole對象中存儲數據與消費者線程從CubbyHole對象中取得數據同步起來。為了達到這一目的,在程序中可以采用監視器(monitor)模型,同時通過調用對象的wait()方法和notify()方法實現同步。
下面是修改后的CubbyHole類的定義:
程序CubbyHole.java
class CubbyHole{
private int content ;
private boolean available=false;
public synchronized void put(int value){
while(available==true){
try{
wait();
}catch(InterruptedException e){}
}
content =value;
available=true;
notifyAll();
}
public synchronized int get(){
while(available==false){
try{
wait();
}catch(InterruptedException e){}
}
available=false;
notifyAll();
return content;
}
}
_____________________________________________________________________________▃
這里有一個boolean型的私有成員變量available用來指示內容是否可取。當available為true時表示數據已經產生還沒被取走,當available為false時表示數據已被取走還沒有存放新的數據。
當生產者線程進入put()方法時,首先檢查available的值,若其為false,才可執行put()方法,若其為true,說明數據還沒有被取走,該線程必須等待。因此在put()方法中調用CubbyHole對象的wait()方法等待。調用對象的wait()方法使線程進入等待狀態,同時釋放對象鎖。直到另一個線程對象調用了notify()或notifyAll()方法,該線程才可恢復運行。
類似地,當消費者線程進入get()方法時,也是先檢查available的值,若其為true,才可執行get()方法,若其為false,說明還沒有數據,該線程必須等待。因此在get()方法中調用CubbyHole對象的wait()方法等待。調用對象的wait()方法使線程進入等待狀態,同時釋放對象鎖。
上述過程就是監視器模型,其中CubbyHole對象為監視器。通過監視器模型可以保證生產者線程和消費者線程同步,結果正確。
程序的運行結果如下:
特別注意:wait()、notify()和notifyAll()方法是Object類定義的方法,并且這些方法只能用在synchronized代碼段中。它們的定義格式如下:
· public final void wait()
· public final void wait(long timeout)
· public final void wait(long timeout, int nanos)
當前線程必須具有對象監視器的鎖,當調用該方法時線程釋放監視器的鎖。調用這些方法使當前線程進入等待(阻塞)狀態,直到另一個線程調用了該對象的notify()方法或notifyAll()方法,該線程重新進入運行狀態,恢復執行。
timeout和nanos為等待的時間的毫秒和納秒,當時間到或其他對象調用了該對象的notify()方法或notifyAll()方法,該線程重新進入運行狀態,恢復執行。
wait()的聲明拋出了InterruptedException,因此程序中必須捕獲或聲明拋出該異常。
· public final void notify()
· public final void notifyAll()
喚醒處于等待該對象鎖的一個或所有的線程繼續執行,通常使用notifyAll()方法。
在生產者/消費者的例子中,CubbyHole類的put和get方法就是臨界區。當生產者修改它時,消費者不能問CubbyHole對象;當消費者取得值時,生產者也不能修改它。