天天看點

Java多線程間的通信

Java多線程間的通信

Java還提供了一種線程間通信的機制,這種通信通什麼實作?

wait,notify等機制  

或使用pipeInputStream和pipeOutputStream

 1. 線程的幾種狀态

線程有四種狀态,任何一個線程肯定處于這四種狀态中的一種:

1) 産生(New):線程對象已經産生,但尚未被啟動,是以無法執行。如通過new産生了一個線程對象後沒對它調用start()函數之前。

2) 可執行(Runnable):每個支援多線程的系統都有一個排程器,排程器會從線程池中選擇一個線程并啟動它。當一個線程處于可執行狀态時,表示它可能正處于線程池中等待排排程器啟動它;也可能它已正在執行。如執行了一個線程對象的start()方法後,線程就處于可執行狀态,但顯而易見的是此時線程不一定正在執行中。

3) 死亡(Dead):當一個線程正常結束,它便處于死亡狀态。如一個線程的run()函數執行完畢後線程就進入死亡狀态。

4) 停滞(Blocked):當一個線程處于停滞狀态時,系統排程器就會忽略它,不對它進行排程。

(另外一篇請看Java線程及多線程技術及應用  http://wenku.baidu.com/view/f214392fb4daa58da0114a32.html?from=rec&pos=1&weight=5&lastweight=3&count=4)

一. 實作多線程

1. 虛假的多線程

例1:

publicclassTestThread

{

inti=0, j=0;

publicvoidgo(intflag){

while(true){

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

if(flag==0)

i++;

java/lang/System.java.html" target="_blank">

System

.out.println("i=" + i);

}

else{

j++;

java/lang/System.java.html" target="_blank">

System

.out.println("j=" + j);

}

}

}

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

newTestThread().go(0);

newTestThread().go(1);

}

}

上面程式的運作結果為:

i=1

i=2

i=3

。。。

結果将一直列印出I的值。我們的意圖是當在while循環中調用sleep()時,另一個線程就将起動,列印出j的值,但結果卻并不是這樣。關于sleep()為什麼不會出現我們預想的結果,在下面将講到。

2. 實作多線程

通過繼承class Thread或實作Runnable接口,我們可以實作多線程

2.1 通過繼承class Thread實作多線程

class Thread中有兩個最重要的函數run()和start()。

1) run()函數必須進行覆寫,把要在多個線程中并行處理的代碼放到這個函數中。

2) 雖然run()函數實作了多個線程的并行處理,但我們不能直接調用run()函數,而是通過調用start()函數來調用run()函數。在調用start()的時候,start()函數會首先進行與多線程相關的初始化(這也是為什麼不能直接調用run()函數的原因),然後再調用run()函數。

例2:

publicclassTestThread extendsjava/lang/Thread.java.html"target="_blank">

Thread

{

privatestaticintthreadCount = 0;

privateintthreadNum = ++threadCount;

privateinti = 5;

publicvoidrun(){

while(true){

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100); 

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

java/lang/System.java.html" target="_blank">

System

.out.println("Thread " + threadNum + " = " + i);

if(--i==0) return;

}

}

>publicstaticvoidmain(java/lang/String.java.html" target="_blank">

String

[] args){

for(inti=0; i<5; i++)

newTestThread().start();

}

}

運作結果為:

Thread 1 = 5

Thread 2 = 5

Thread 3 = 5

Thread 4 = 5

Thread 5 = 5

Thread 1 = 4

Thread 2 = 4

Thread 3 = 4

Thread 4 = 4

Thread 1 = 3

Thread 2 = 3

Thread 5 = 4

Thread 3 = 3

Thread 4 = 3

Thread 1 = 2

Thread 2 = 2

Thread 5 = 3

Thread 3 = 2

Thread 4 = 2

Thread 1 = 1

Thread 2 = 1

Thread 5 = 2

Thread 3 = 1

Thread 4 = 1

Thread 5 = 1

從結果可見,例2能實作多線程的并行處理。

**:在上面的例子中,我們隻用new産生Thread對象,并沒有用reference來記錄所産生的Thread對象。根據垃圾回收機制,當一個對象沒有被reference引用時,它将被回收。但是垃圾回收機制對Thread對象“不成立”。因為每一個Thread都會進行注冊動作,是以即使我們在産生Thread對象時沒有指定一個reference指向這個對象,實際上也會在某個地方有個指向該對象的reference,是以垃圾回收器無法回收它們。

3) 通過Thread的子類産生的線程對象是不同對象的線程

classTestSynchronized extendsjava/lang/Thread.java.html"target="_blank">

Thread

{

publicTestSynchronized(java/lang/String.java.html"target="_blank">

String

name){

super(name);

}

publicsynchronizedstaticvoidprt(){

for(inti=10; i<20; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

publicsynchronizedvoidrun(){

for(inti=0; i<3; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestSynchronized t1 = newTestSynchronized("t1");

TestSynchronized t2 = newTestSynchronized("t2");

t1.start();

t1.start(); //(1)

//t2.start(); (2)

}

}

運作結果為:

t1 : 0

t1 : 1

t1 : 2

t1 : 0

t1 : 1

t1 : 2

由于是同一個對象啟動的不同線程,是以run()函數實作了synchronized。如果去掉(2)的注釋,把代碼(1)注釋掉,結果将變為:

t1 : 0

t2 : 0

t1 : 1

t2 : 1

t1 : 2

t2 : 2

由于t1和t2是兩個對象,是以它們所啟動的線程可同時通路run()函數。

2.2 通過實作Runnable接口實作多線程

如果有一個類,它已繼承了某個類,又想實作多線程,那就可以通過實作Runnable接口來實作。

1) Runnable接口隻有一個run()函數。

2) 把一個實作了Runnable接口的對象作為參數産生一個Thread對象,再調用Thread對象的start()函數就可執行并行操作。如果在産生一個Thread對象時以一個Runnable接口的實作類的對象作為參數,那麼在調用start()函數時,start()會調用Runnable接口的實作類中的run()函數。

例3.1:

publicclassTestThread implementsjava/lang/Runnable.java.html"target="_blank">

Runnable

{

privatestaticintthreadCount = 0;

privateintthreadNum = ++threadCount;

privateinti = 5;

publicvoidrun(){

while(true){

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

java/lang/System.java.html" target="_blank">

System

.out.println("Thread " + threadNum + " = " + i);

if(--i==0) return;

}

}

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

for(inti=0; i<5; i++)

newjava/lang/Thread.java.html" target="_blank">

Thread

(newTestThread()).start(); //(1)

}

}

運作結果為:

Thread 1 = 5

Thread 2 = 5

Thread 3 = 5

Thread 4 = 5

Thread 5 = 5

Thread 1 = 4

Thread 2 = 4

Thread 3 = 4

Thread 4 = 4

Thread 4 = 3

Thread 5 = 4

Thread 1 = 3

Thread 2 = 3

Thread 3 = 3

Thread 4 = 2

Thread 5 = 3

Thread 1 = 2

Thread 2 = 2

Thread 3 = 2

Thread 4 = 1

Thread 5 = 2

Thread 1 = 1

Thread 2 = 1

Thread 3 = 1

Thread 5 = 1

例3是對例2的修改,它通過實作Runnable接口來實作并行處理。代碼(1)處可見,要調用TestThread中的并行操作部分,要把一個TestThread對象作為參數來産生Thread對象,再調用Thread對象的start()函數。

3) 同一個實作了Runnable接口的對象作為參數産生的所有Thread對象是同一對象下的線程。

例3.2:

packagemypackage1;

publicclassTestThread implementsjava/lang/Runnable.java.html"target="_blank">

Runnable

{

publicsynchronizedvoidrun(){

for(inti=0; i<5; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestThread testThread = newTestThread();

for(inti=0; i<5; i++)

//new Thread(testThread, "t" + i).start(); (1)

newjava/lang/Thread.java.html" target="_blank">

Thread

(newTestThread(), "t" + i).start(); (2)

}

}

運作結果為:

t0 : 0

t1 : 0

t2 : 0

t3 : 0

t4 : 0

t0 : 1

t1 : 1

t2 : 1

t3 : 1

t4 : 1

t0 : 2

t1 : 2

t2 : 2

t3 : 2

t4 : 2

t0 : 3

t1 : 3

t2 : 3

t3 : 3

t4 : 3

t0 : 4

t1 : 4

t2 : 4

t3 : 4

t4 : 4

由于代碼(2)每次都是用一個新的TestThread對象來産生Thread對象的,是以産生出來的Thread對象是不同對象的線程,是以所有Thread對象都可同時通路run()函數。如果注釋掉代碼(2),并去掉代碼(1)的注釋,結果為:

t0 : 0

t0 : 1

t0 : 2

t0 : 3

t0 : 4

t1 : 0

t1 : 1

t1 : 2

t1 : 3

t1 : 4

t2 : 0

t2 : 1

t2 : 2

t2 : 3

t2 : 4

t3 : 0

t3 : 1

t3 : 2

t3 : 3

t3 : 4

t4 : 0

t4 : 1

t4 : 2

t4 : 3

t4 : 4

由于代碼(1)中每次都是用同一個TestThread對象來産生Thread對象的,是以産生出來的Thread對象是同一個對象的線程,是以實作run()函數的同步。

二. 共享資源的同步

1. 同步的必要性

例4:

classSeq{

privatestaticintnumber = 0;

privatestaticSeq seq = newSeq();

privateSeq() {}

publicstaticSeq getInstance(){

returnseq;

}

publicintget(){

number++;  //(a)

returnnumber; //(b)

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

Seq.getInstance().get(); //(1)

Seq.getInstance().get(); //(2)

}

}

上面是一個取得序列号的單例模式的例子,但調用get()時,可能會産生兩個相同的序列号:

當代碼(1)和(2)都試圖調用get()取得一個唯一的序列。當代碼(1)執行完代碼(a),正要執行代碼(b)時,它被中斷了并開始執行代碼(2)。一旦當代碼(2)執行完(a)而代碼(1)還未執行代碼(b),那麼代碼(1)和代碼(2)就将得到相同的值。

2. 通過synchronized實作資源同步

2.1 鎖标志

2.1.1 每個對象都有一個标志鎖。當對象的一個線程通路了對象的某個synchronized資料(包括函數)時,這個對象就将被“上鎖”,是以被聲明為synchronized的資料(包括函數)都不能被調用(因為目前線程取走了對象的“鎖标志”)。隻有目前線程通路完它要通路的synchronized資料,釋放“鎖标志”後,同一個對象的其它線程才能通路synchronized資料。

2.1.2 每個class也有一個“鎖标志”。對于synchronized static資料(包括函數)可以在整個class下進行鎖定,避免static資料的同時通路。

例5:

classSeq{

privatestaticintnumber = 0;

privatestaticSeq seq = newSeq();

privateSeq() {}

publicstaticSeq getInstance(){

returnseq;

}

publicsynchronizedintget(){ //(1)

number++;

returnnumber;

}

}

例5在例4的基礎上,把get()函數聲明為synchronized,那麼在同一個對象中,就隻能有一個線程調用get()函數,是以每個線程取得的number值就是唯一的了。

例6:

classSeq{

privatestaticintnumber = 0;

privatestaticSeq seq = null;

privateSeq() {}

synchronizedpublicstaticSeq getInstance(){ //(1)

if(seq==null) seq = newSeq();

returnseq;

}

publicsynchronizedintget(){

number++;

returnnumber;

}

}

例6把getInstance()函數聲明為synchronized,那樣就保證通過getInstance()得到的是同一個seq對象。

2.2 non-static的synchronized資料隻能在同一個對象的純種實作同步通路,不同對象的線程仍可同時通路。

例7:

classTestSynchronized implementsjava/lang/Runnable.java.html"target="_blank">

Runnable

{

publicsynchronizedvoidrun(){ //(1)

for(inti=0; i<10; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestSynchronized r1 = newTestSynchronized();

TestSynchronized r2 = newTestSynchronized();

java/lang/Thread.java.html" target="_blank">

Thread

t1 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r1, "t1");

java/lang/Thread.java.html" target="_blank">

Thread

t2 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r2, "t2"); //(3)

//Thread t2 = new Thread(r1, "t2"); (4)

t1.start();

t2.start();

}

}

運作結果為:

t1 : 0

t2 : 0

t1 : 1

t2 : 1

t1 : 2

t2 : 2

t1 : 3

t2 : 3

t1 : 4

t2 : 4

t1 : 5

t2 : 5

t1 : 6

t2 : 6

t1 : 7

t2 : 7

t1 : 8

t2 : 8

t1 : 9

t2 : 9

雖然我們在代碼(1)中把run()函數聲明為synchronized,但由于t1、t2是兩個對象(r1、r2)的線程,而run()函數是non-static的synchronized資料,是以仍可被同時通路(代碼(2)中的sleep()函數由于在暫停時不會釋放“标志鎖”,因為線程中的循環很難被中斷去執行另一個線程,是以代碼(2)隻是為了顯示結果)。

如果把例7中的代碼(3)注釋掉,并去年代碼(4)的注釋,運作結果将為:

t1 : 0

t1 : 1

t1 : 2

t1 : 3

t1 : 4

t1 : 5

t1 : 6

t1 : 7

t1 : 8

t1 : 9

t2 : 0

t2 : 1

t2 : 2

t2 : 3

t2 : 4

t2 : 5

t2 : 6

t2 : 7

t2 : 8

t2 : 9

修改後的t1、t2是同一個對象(r1)的線程,是以隻有當一個線程(t1或t2中的一個)執行run()函數,另一個線程才能執行。

2.3 對象的“鎖标志”和class的“鎖标志”是互相獨立的。

例8:

classTestSynchronized extendsjava/lang/Thread.java.html"target="_blank">

Thread

{

publicTestSynchronized(java/lang/String.java.html"target="_blank">

String

name){

super(name);

}

publicsynchronizedstaticvoidprt(){

for(inti=10; i<20; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

publicsynchronizedvoidrun(){

for(inti=0; i<10; i++){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + i);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestSynchronized t1 = newTestSynchronized("t1");

TestSynchronized t2 = newTestSynchronized("t2");

t1.start();

t1.prt(); //(1)

t2.prt(); //(2)

}

}

運作結果為:

main : 10

t1 : 0

main : 11

t1 : 1

main : 12

t1 : 2

main : 13

t1 : 3

main : 14

t1 : 4

main : 15

t1 : 5

main : 16

t1 : 6

main : 17

t1 : 7

main : 18

t1 : 8

main : 19

t1 : 9

main : 10

main : 11

main : 12

main : 13

main : 14

main : 15

main : 16

main : 17

main : 18

main : 19

在代碼(1)中,雖然是通過對象t1來調用prt()函數的,但由于prt()是靜态的,是以調用它時不用經過任何對象,它所屬的線程為main線程。

由于調用run()函數取走的是對象鎖,而調用prt()函數取走的是class鎖,是以同一個線程t1(由上面可知實際上是不同線程)調用run()函數且還沒完成run()函數時,它就能調用prt()函數。但prt()函數隻能被一個線程調用,如代碼(1)和代碼(2),即使是兩個不同的對象也不能同時調用prt()。

3. 同步的優化

1) synchronized block

文法為:synchronized(reference){do this }

reference用來指定“以某個對象的鎖标志”對“大括号内的代碼”實施同步控制。

例9:

classTestSynchronized implementsjava/lang/Runnable.java.html"target="_blank">

Runnable

{

staticintj = 0;

publicsynchronizedvoidrun(){

for(inti=0; i<5; i++){

//(1)

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + j++);

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestSynchronized r1 = newTestSynchronized();

TestSynchronized r2 = newTestSynchronized();

java/lang/Thread.java.html" target="_blank">

Thread

t1 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r1, "t1");

java/lang/Thread.java.html" target="_blank">

Thread

t2 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r1, "t2");

t1.start();

t2.start();

}

}

運作結果為:

t1 : 0

t1 : 1

t1 : 2

t1 : 3

t1 : 4

t2 : 5

t2 : 6

t2 : 7

t2 : 8

t2 : 9

上面的代碼的run()函數實作了同步,使每次列印出來的j總是不相同的。但實際上在整個run()函數中,我們隻關心j的同步,而其餘代碼同步與否我們是不關心的,是以可以對它進行以下修改:

classTestSynchronized implementsjava/lang/Runnable.java.html"target="_blank">

Runnable

{

staticintj = 0;

publicvoidrun(){

for(inti=0; i<5; i++){

//(1)

synchronized(this){

java/lang/System.java.html" target="_blank">

System

.out.println(java/lang/Thread.java.html" target="_blank">

Thread

.currentThread().getName() + " : " + j++);

}

try{

java/lang/Thread.java.html" target="_blank">

Thread

.sleep(100);

}

catch(java/lang/InterruptedException.java.html"target="_blank">

InterruptedException

e){

java/lang/System.java.html" target="_blank">

System

.out.println("Interrupted");

}

}

}

}

publicclassTestThread{

publicstaticvoidmain(java/lang/String.java.html"target="_blank">

String

[] args){

TestSynchronized r1 = newTestSynchronized();

TestSynchronized r2 = newTestSynchronized();

java/lang/Thread.java.html" target="_blank">

Thread

t1 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r1, "t1");

java/lang/Thread.java.html" target="_blank">

Thread

t2 = newjava/lang/Thread.java.html" target="_blank">

Thread

(r1, "t2");

t1.start();

t2.start();

}

}

運作結果為:

t1 : 0

t2 : 1

t1 : 2

t2 : 3

t1 : 4

t2 : 5

t1 : 6

t2 : 7

t1 : 8

t2 : 9

由于進行同步的範圍縮小了,是以程式的效率将提高。同時,代碼(1)指出,當對大括号内的println()語句進行同步控制時,會取走目前對象的“鎖标志”,即對目前對象“上鎖”,不讓目前對象下的其它線程執行目前對象的其它synchronized資料。

三. 線程間的通信

1. 線程的幾種狀态

線程有四種狀态,任何一個線程肯定處于這四種狀态中的一種:

1) 産生(New):線程對象已經産生,但尚未被啟動,是以無法執行。如通過new産生了一個線程對象後沒對它調用start()函數之前。

2) 可執行(Runnable):每個支援多線程的系統都有一個排程器,排程器會從線程池中選擇一個線程并啟動它。當一個線程處于可執行狀态時,表示它可能正處于線程池中等待排排程器啟動它;也可能它已正在執行。如執行了一個線程對象的start()方法後,線程就處于可執行狀态,但顯而易見的是此時線程不一定正在執行中。

3) 死亡(Dead):當一個線程正常結束,它便處于死亡狀态。如一個線程的run()函數執行完畢後線程就進入死亡狀态。

4) 停滞(Blocked):當一個線程處于停滞狀态時,系統排程器就會忽略它,不對它進行排程。當處于停滞狀态的線程重新回到可執行狀态時,它有可能重新執行。如通過對一個線程調用wait()函數後,線程就進入停滞狀态,隻有當兩次對該線程調用notify或notifyAll後它才能兩次回到可執行狀态。

2. class Thread下的常用函數函數

2.1 suspend()、resume()

1) 通過suspend()函數,可使線程進入停滞狀态。通過suspend()使線程進入停滞狀态後,除非收到resume()消息,否則該線程不會變回可執行狀态。

2) 當調用suspend()函數後,線程不會釋放它的“鎖标志”。

例11:

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public synchronized void run(){

if(shareVar==0){

for(int i=0; i<5; i++){

shareVar++;

if(shareVar==5){

this.suspend(); //(1)

}

}

}

else{

System.out.print(Thread.currentThread().getName());

System.out.println(" shareVar = " + shareVar);

this.resume(); //(2)

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.start(); //(5)

//t1.start(); //(3)

t2.start(); //(4)

}

}

運作結果為:

t2 shareVar = 5

i. 當代碼(5)的t1所産生的線程運作到代碼(1)處時,該線程進入停滞狀态。然後排程器從線程池中喚起代碼(4)的t2所産生的線程,此時shareVar值不為0,是以執行else中的語句。

ii. 也許你會問,那執行代碼(2)後為什麼不會使t1進入可執行狀态呢?正如前面所說,t1和t2是兩個不同對象的線程,而代碼(1)和(2)都隻對目前對象進行操作,是以t1所産生的線程執行代碼(1)的結果是對象t1的目前線程進入停滞狀态;而t2所産生的線程執行代碼(2)的結果是把對象t2中的所有處于停滞狀态的線程調回到可執行狀态。

iii. 那現在把代碼(4)注釋掉,并去掉代碼(3)的注釋,是不是就能使t1重新回到可執行狀态呢?運作結果是什麼也不輸出。為什麼會這樣呢?也許你會認為,當代碼(5)所産生的線程執行到代碼(1)時,它進入停滞狀态;而代碼(3)所産生的線程和代碼(5)所産生的線程是屬于同一個對象的,那麼就當代碼(3)所産生的線程執行到代碼(2)時,就可使代碼(5)所産生的線程執行回到可執行狀态。但是要清楚,suspend()函數隻是讓目前線程進入停滞狀态,但并不釋放目前線程所獲得的“鎖标志”。是以當代碼(5)所産生的線程進入停滞狀态時,代碼(3)所産生的線程仍不能啟動,因為目前對象的“鎖标志”仍被代碼(5)所産生的線程占有。

2.2 sleep()

1) sleep ()函數有一個參數,通過參數可使線程在指定的時間内進入停滞狀态,當指定的時間過後,線程則自動進入可執行狀态。

2) 當調用sleep ()函數後,線程不會釋放它的“鎖标志”。

例12:

class TestThreadMethod extendsThread{

class TestThreadMethod extends Thread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public synchronized void run(){

for(int i=0; i<3; i++){

System.out.print(Thread.currentThread().getName());

System.out.println(" : " + i);

try{

Thread.sleep(100); //(4)

}

catch(InterruptedException e){

System.out.println("Interrupted");

}

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.start(); (1)

t1.start(); (2)

//t2.start(); (3)

}

}

運作結果為:

t1 : 0

t1 : 1

t1 : 2

t1 : 0

t1 : 1

t1 : 2

由結果可證明,雖然在run()中執行了sleep(),但是它不會釋放對象的“鎖标志”,是以除非代碼(1)的線程執行完run()函數并釋放對象的“鎖标志”,否則代碼(2)的線程永遠不會執行。

如果把代碼(2)注釋掉,并去掉代碼(3)的注釋,結果将變為:

t1 : 0

t2 : 0

t1 : 1

t2 : 1

t1 : 2

t2 : 2

由于t1和t2是兩個對象的線程,是以當線程t1通過sleep()進入停滞時,排程器會從線程池中調用其它的可執行線程,進而t2線程被啟動。

例13:

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public synchronized void run(){

for(int i=0; i<5; i++){

System.out.print(Thread.currentThread().getName());

System.out.println(" : " + i);

try{

if(Thread.currentThread().getName().equals("t1"))

Thread.sleep(200);

else

Thread.sleep(100);

}

catch(InterruptedException e){

System.out.println("Interrupted");

}

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.start();

//t1.start();

t2.start();

}

}

運作結果為:

t1 : 0

t2 : 0

t2 : 1

t1 : 1

t2 : 2

t2 : 3

t1 : 2

t2 : 4

t1 : 3

t1 : 4

由于線程t1調用了sleep(200),而線程t2調用了sleep(100),是以線程t2處于停滞狀态的時間是線程t1的一半,從從結果反映出來的就是線程t2列印兩倍次線程t1才列印一次。

2.3 yield()

1) 通過yield ()函數,可使線程進入可執行狀态,排程器從可執行狀态的線程中重新進行排程。是以調用了yield()的函數也有可能馬上被執行。

2) 當調用yield ()函數後,線程不會釋放它的“鎖标志”。

例14:

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public synchronized void run(){

for(int i=0; i<4; i++){

System.out.print(Thread.currentThread().getName());

System.out.println(" : " + i);

Thread.yield();

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.start();

t1.start(); //(1)

//t2.start(); (2)

}

}

運作結果為:

t1 : 0

t1 : 1

t1 : 2

t1 : 3

t1 : 0

t1 : 1

t1 : 2

t1 : 3

從結果可知調用yield()時并不會釋放對象的“鎖标志”。

如果把代碼(1)注釋掉,并去掉代碼(2)的注釋,結果為:

t1 : 0

t1 : 1

t2 : 0

t1 : 2

t2 : 1

t1 : 3

t2 : 2

t2 : 3

從結果可知,雖然t1線程調用了yield(),但它馬上又被執行了。

2.4 sleep()和yield()的差別

1) sleep()使目前線程進入停滞狀态,是以執行sleep()的線程在指定的時間内肯定不會執行;yield()隻是使目前線程重新回到可執行狀态,是以執行yield()的線程有可能在進入到可執行狀态後馬上又被執行。

2) sleep()可使優先級低的線程得到執行的機會,當然也可以讓同優先級和高優先級的線程有執行的機會;yield()隻能使同優先級的線程有執行的機會。

例15:

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public void run(){

for(int i=0; i<4; i++){

System.out.print(Thread.currentThread().getName());

System.out.println(" : " + i);

//Thread.yield(); (1)

try{

Thread.sleep(3000);

}

catch(InterruptedException e){

System.out.println("Interrupted");

}

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.setPriority(Thread.MAX_PRIORITY);

t2.setPriority(Thread.MIN_PRIORITY);

t1.start();

t2.start();

}

}

運作結果為:

t1 : 0

t1 : 1

t2 : 0

t1 : 2

t2 : 1

t1 : 3

t2 : 2

t2 : 3

由結果可見,通過sleep()可使優先級較低的線程有執行的機會。注釋掉代碼(2),并去掉代碼(1)的注釋,結果為:

t1 : 0

t1 : 1

t1 : 2

t1 : 3

t2 : 0

t2 : 1

t2 : 2

t2 : 3

可見,調用yield(),不同優先級的線程永遠不會得到執行機會。

2.5 join()

使調用join()的線程執行完畢後才能執行其它線程,在一定意義上,它可以實作同步的功能。

例16:

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public void run(){

for(int i=0; i<4; i++){

System.out.println(" " + i);

try{

Thread.sleep(3000);

}

catch(InterruptedException e){

System.out.println("Interrupted");

}

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

t1.start();

try{

t1.join();

}

catch(InterruptedException e){}

t1.start();

}

}

運作結果為:

1

2

3

1

2

3

3. class Object下常用的線程函數

wait()、notify()和notifyAll()這三個函數由java.lang.Object類提供,用于協調多個線程對共享資料的存取。

3.1 wait()、notify()和notifyAll()

1) wait()函數有兩種形式:第一種形式接受一個毫秒值,用于在指定時間長度内暫停線程,使線程進入停滞狀态。第二種形式為不帶參數,代表waite()在notify()或notifyAll()之前會持續停滞。

2) 當對一個對象執行notify()時,會從線程等待池中移走該任意一個線程,并把它放到鎖标志等待池中;當對一個對象執行notifyAll()時,會從線程等待池中移走所有該對象的所有線程,并把它們放到鎖标志等待池中。

3) 當調用wait()後,線程會釋放掉它所占有的“鎖标志”,進而使線程所在對象中的其它synchronized資料可被别的線程使用。

例17:

下面,我們将對例11中的例子進行修改

class TestThreadMethod extendsThread{

public static int shareVar = 0;

public TestThreadMethod(String name){

super(name);

}

public synchronized void run(){

if(shareVar==0){

for(int i=0; i<10; i++){

shareVar++;

if(shareVar==5){

try{

this.wait(); //(4)

}

catch(InterruptedException e){}

}

}

}

if(shareVar!=0){

System.out.print(Thread.currentThread().getName());

System.out.println(" shareVar = " + shareVar);

this.notify(); //(5)

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

TestThreadMethod t2 = new TestThreadMethod("t2");

t1.start(); //(1)

//t1.start(); (2)

t2.start(); //(3)

}

}

運作結果為:

t2 shareVar = 5

因為t1和t2是兩個不同對象,是以線程t2調用代碼(5)不能喚起線程t1。如果去掉代碼(2)的注釋,并注釋掉代碼(3),結果為:

t1 shareVar = 5

t1 shareVar = 10

這是因為,當代碼(1)的線程執行到代碼(4)時,它進入停滞狀态,并釋放對象的鎖狀态。接着,代碼(2)的線程執行run(),由于此時shareVar值為5,是以執行列印語句并調用代碼(5)使代碼(1)的線程進入可執行狀态,然後代碼(2)的線程結束。當代碼(1)的線程重新執行後,它接着執行for()循環一直到shareVar=10,然後列印shareVar。

3.2 wait()、notify()和synchronized

waite()和notify()因為會對對象的“鎖标志”進行操作,是以它們必須在synchronized函數或synchronized block中進行調用。如果在non-synchronized函數或non-synchronized block中進行調用,雖然能編譯通過,但在運作時會發生IllegalMonitorStateException的異常。

例18:

class TestThreadMethod extendsThread{

public int shareVar = 0;

public TestThreadMethod(String name){

super(name);

new Notifier(this);

}

public synchronized void run(){

if(shareVar==0){

for(int i=0; i<5; i++){

shareVar++;

System.out.println("i = " + shareVar);

try{

System.out.println("wait......");

this.wait();

}

catch(InterruptedException e){}

}

}

}

}

class Notifier extends Thread{

private TestThreadMethod ttm;

Notifier(TestThreadMethod t){

ttm = t;

start();

}

public void run(){

while(true){

try{

sleep(2000);

}

catch(InterruptedException e){}

synchronized(ttm){

System.out.println("notify......");

ttm.notify();

}

}

}

}

public class TestThread{

public static void main(String[] args){

TestThreadMethod t1 = new TestThreadMethod("t1");

t1.start();

}

}

運作結果為:

i = 1

wait......

notify......

i = 2

wait......

notify......

i = 3

wait......

notify......

i = 4

wait......

notify......

i = 5

wait......

notify......

4. wait()、notify()、notifyAll()和suspend()、resume()、sleep()的讨論

4.1 這兩組函數的差別

1) wait()使目前線程進入停滞狀态時,還會釋放目前線程所占有的“鎖标志”,進而使線程對象中的synchronized資源可被對象中别的線程使用;而suspend()和sleep()使目前線程進入停滞狀态時不會釋放目前線程所占有的“鎖标志”。

2) 前一組函數必須在synchronized函數或synchronized block中調用,否則在運作時會産生錯誤;而後一組函數可以non-synchronized函數和synchronized block中調用。

4.2 這兩組函數的取舍

Java2已不建議使用後一組函數。因為在調用wait()時不會釋放目前線程所取得的“鎖标志”,這樣很容易造成“死鎖”。

最後,問題

線程之間的關系是平等的,彼此之間并不存在任何依賴,它們各自競争CPU資源,互不相讓,并且還無條件地阻止其他線程對共享資源的異步通路。然而,也有很多現實問題要求不僅要同步的通路同一共享資源,而且線程間還彼此牽制,通過互相通信來向前推進。那麼,多個線程之間是如何進行通信的呢?

解決思路

在現實應用中,很多時候都需要讓多個線程按照一定的次序來通路共享資源,例如,經典的生産者和消費者問題。這類問題描述了這樣一種情況,假設倉庫中隻能存放一件産品,生産者将生産出來的産品放入倉庫,消費者将倉庫中的産品取走消費。如果倉庫中沒有産品,則生産者可以将産品放入倉庫,否則停止生産并等待,直到倉庫中的産品被消費者取走為止。如果倉庫中放有産品,則消費者可以将産品取走消費,否則停止消費并等待,直到倉庫中再次放入産品為止。顯然,這是一個同步問題,生産者和消費者共享同一資源,并且,生産者和消費者之間彼此依賴,互為條件向前推進。但是,該如何編寫程式來解決這個問題呢?

傳統的思路是利用循環檢測的方式來實作,這種方式通過重複檢查某一個特定條件是否成立來決定線程的推進順序。比如,一旦生産者生産結束,它就繼續利用循環檢測來判斷倉庫中的産品是否被消費者消費,而消費者也是在消費結束後就會立即使用循環檢測的方式來判斷倉庫中是否又放進産品。顯然,這些操作是很耗費CPU資源的,不值得提倡。那麼有沒有更好的方法來解決這類問題呢?

首先,當線程在繼續執行前需要等待一個條件方可繼續執行時,僅有synchronized 關鍵字是不夠的。因為雖然synchronized關鍵字可以阻止并發更新同一個共享資源,實作了同步,但是它不能用來實作線程間的消息傳遞,也就是所謂的通信。而在處理此類問題的時候又必須遵循一種原則,即:對于生産者,在生産者沒有生産之前,要通知消費者等待;在生産者生産之後,馬上又通知消費者消費;對于消費者,在消費者消費之後,要通知生産者已經消費結束,需要繼續生産新的産品以供消費。

其實,Java提供了3個非常重要的方法來巧妙地解決線程間的通信問題。這3個方法分别是:wait()、notify()和notifyAll()。它們都是Object類的最終方法,是以每一個類都預設擁有它們。

雖然所有的類都預設擁有這3個方法,但是隻有在synchronized關鍵字作用的範圍内,并且是同一個同步問題中搭配使用這3個方法時才有實際的意義。

這些方法在Object類中聲明的文法格式如下所示:

final void wait() throws InterruptedException

final void notify()

final void notifyAll()

其中,調用wait()方法可以使調用該方法的線程釋放共享資源的鎖,然後從運作态退出,進入等待隊列,直到被再次喚醒。而調用notify()方法可以喚醒等待隊列中第一個等待同一共享資源的線程,并使該線程退出等待隊列,進入可運作态。調用notifyAll()方法可以使所有正在等待隊列中等待同一共享資源的線程從等待狀态退出,進入可運作狀态,此時,優先級最高的那個線程最先執行。顯然,利用這些方法就不必再循環檢測共享資源的狀态,而是在需要的時候直接喚醒等待隊列中的線程就可以了。這樣不但節省了寶貴的CPU資源,也提高了程式的效率。

由于wait()方法在聲明的時候被聲明為抛出InterruptedException異常,是以,在調用wait()方法時,需要将它放入try…catch代碼塊中。此外,使用該方法時還需要把它放到一個同步代碼段中,否則會出現如下異常:

"java.lang.IllegalMonitorStateException: current thread notowner"

這些方法是不是就可以實作線程間的通信了呢?下面将通過多線程同步的模型:生産者和消費者問題來說明怎樣通過程式解決多線程間的通信問題。

具體步驟

下面這個程式示範了多個線程之間進行通信的具體實作過程。程式中用到了4個類,其中ShareData類用來定義共享資料和同步方法。在同步方法中調用了wait()方法和notify()方法,并通過一個信号量來實作線程間的消息傳遞。

// 例4.6.1CommunicationDemo.java 描述:生産者和消費者之間的消息傳遞過程

class ShareData

{

private char c;

private boolean isProduced = false; // 信号量

public synchronized void putShareChar(char c) // 同步方法putShareChar()

{

if (isProduced) // 如果産品還未消費,則生産者等待

{

try

{

wait(); // 生産者等待

}catch(InterruptedException e){

e.printStackTrace();

}

}

this.c = c;

isProduced = true; // 标記已經生産

notify(); // 通知消費者已經生産,可以消費

}

public synchronized char getShareChar() // 同步方法getShareChar()

{

if (!isProduced) // 如果産品還未生産,則消費者等待

{

try

{

wait(); // 消費者等待

}catch(InterruptedException e){

e.printStackTrace();

}

}

isProduced = false; // 标記已經消費

notify(); // 通知需要生産

return this.c;

}

}

class Producer extends Thread // 生産者線程

{

private ShareData s;

Producer(ShareData s)

{

this.s = s;

}

public void run()

{

for (char ch = ''A''; ch <= ''D''; ch++)

{

try

{

Thread.sleep((int)(Math.random()*3000));

}catch(InterruptedException e){

e.printStackTrace();

}

s.putShareChar(ch); // 将産品放入倉庫

System.out.println(ch + " is produced by Producer.");

}

}

}

class Consumer extends Thread // 消費者線程

{

private ShareData s;

Consumer(ShareData s)

{

this.s = s;

}

public void run()

{

char ch;

do{

try

{

Thread.sleep((int)(Math.random()*3000));

}catch(InterruptedException e){

e.printStackTrace();

}

ch = s.getShareChar(); // 從倉庫中取出産品

System.out.println(ch + " is consumed by Consumer. ");

}while (ch != ''D'');

}

}

class CommunicationDemo

{

public static void main(String[] args)

{

ShareData s = new ShareData();

new Consumer(s).start();

new Producer(s).start();

}

}