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