線程間的通信 : 其實就是多個線程操作同一資源,但是操作的動作不同。
注意:
1、多個線程
2、同一資源
3、操作動作不同
例如一個場景: 輸入線程(Input) 和 輸出線程(outPut) ,輸入線程負責操作資源(Resource),Resource 有 name(String) 和 sex(String)兩個變量,輸入線程(Input) 負責爲 Resource 的成員賦值,輸出線程 (outPut)打印值。
分析 : 按照正常的場景來說,輸入線程(Input)一旦爲資源(Resource)賦值完成後,輸出線程(outPut) 應該打印資源(Resource)的成員變量值。同一資源,可通過構造函數將資源傳入線程。
先看如下代碼:
//資源
class Resource
{
String name;
String sex;
}
//輸入線程
class Input implements Runnable
{
private Resource res;
// 保證 同一資源
public Input(Resource res)
{
this.res = res;
}
public void run()
{
int x = 0;
// 死循環的原因: 打印越多越容易看出問題所在
while(true)
{
if(x==0)
{
res.name ="jack";
res.sex = "man";
}
else
{
res.name = "傑克";
res.sex = "男";
}
// 目的在於做切換,賦值不同是爲了更好區分是否存在問題
x = (x+1)%2;
}
}
}
//輸出線程
class outPut implements Runnable
{
private Resource res;
public outPut(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
System.out.println("輸出線程:"+this.res.name+"..."+this.res.sex);
}
}
}
//主函數
public class ThreadWaitAndNotify
{
public static void main(String[] args)
{
//資源
Resource res = new Resource();
Input input = new Input(res);
outPut output = new outPut(res);
// 輸入線程
Thread t1 = new Thread(input);
//輸出線程
Thread t2 = new Thread(output);
t1.start();
t2.start();
}
}
看下打印結果(截取部分):
輸出線程:傑克...man
輸出線程:jack...男
輸出線程:傑克...男
輸出線程:jack...man
輸出線程:傑克...男
輸出線程:傑克...man
分析: 從打印結果上來說,沒達到我們的要求,正常來說,應該是中文對應中文,英文對應英文輸出纔對 ! 那爲什麼會出現如下問題呢 ? 其實,原因很簡單,假設輸入線程 (Input)第一次 賦值 name = “jack”,sex = “man”,此時輸入線程(Input)繼續享有執行權,做切換賦值,當賦值到 name = “傑克”時,即將進行sex = “男”賦值時,失去了執行權,此時,cpu執行權被輸出線程(outPut)拿到,輸出操作資源(res)的成員變量值,由於是操作同一資源,此時資源(res)的 name = “傑克”, sex = “man”,將其輸出。導致瞭如上結果。
解決方案: 同步。那麼同步應該注意哪些地方呢 ?
( 1 ) 首先明白 線程 對資源的操作 代碼 是哪些,這個比較好解決,就是 run方法的代碼。
( 2 ) 其次,鎖必須唯一。那有兩種選擇,可以是res對象,也可以是字節碼文件對象(在內存中,InPut.class和ouput.class也是唯一存在的)
修改代碼如下:
//資源
class Resource
{
String name;
String sex;
}
//輸入線程
class Input implements Runnable
{
private Resource res;
public Input(Resource res)
{
this.res = res;
}
public void run()
{
int x = 0;
while(true)
{
synchronized (res)
{
if(x==0)
{
res.name ="jack";
res.sex = "man";
}
else
{
res.name = "傑克";
res.sex = "男";
}
x = (x+1)%2;
}
}
}
}
//輸出線程
class outPut implements Runnable
{
private Resource res;
public outPut(Resource res)
{
this.res = res;
}
@Override
public void run()
{
while(true)
{
synchronized (res)
{
System.out.println("輸出線程:"+this.res.name+"..."+this.res.sex);
}
}
}
}
//主函數
public class ThreadWaitAndNotify
{
public static void main(String[] args)
{
Resource res = new Resource();
Input input = new Input(res);
outPut output = new outPut(res);
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
t1.start();
t2.start();
}
}
打印結果(截取部分):
輸出線程:jack...man
輸出線程:jack...man
輸出線程:jack...man
輸出線程:jack...man
輸出線程:傑克...男
輸出線程:傑克...男
輸出線程:傑克...男
線程安全問題是解決了,但是,跟我們預期的結果還是有些差距? 回到最初文章開頭我描述的場景,我們期望的結果是,輸入線程(Input)順利對操作資源(res)賦值完成後,輸出線程(outPut)馬上能夠打印輸入線程的結果。
如下:
輸出線程:jack...man
輸出線程:傑克...男
輸出線程:jack...man
輸出線程:傑克...男
輸出線程:jack...man
輸出線程:傑克...男
輸出線程:jack...man
.
.
.
爲什麼我們解決了線程安全問題,但是沒達到我們想要的結果呢?
分析這個原因也很簡單 : 當輸入線程(Input)操作資源(res)完成後,假設輸入線程(Input)這時候失去執行權,而輸出線程(outPut)在短時間內得到了執行權,就會瘋狂打印這個結果。如何解決呢?這個涉及到一個知識點,線程的等待喚醒機制。下篇博文介紹。