Java中的多執行緒(執行緒間通訊)
/學習筆記/
執行緒間通訊:
多個執行緒在處理同一資源,但是任務卻不同。 先看一個例子,採用兩個執行緒執行進行輸入和輸出任務:
//資源 class Resource { String name; String sex; } //輸入 class Input implements Runnable { Resource r ; // Object obj = new Object(); Input(Resource r) { this.r = r; } public void run() { int x = 0; while(true) { if(x==0) { r.name = "mike"; r.sex = "nan"; } else { r.name = "麗麗"; r.sex = "女女女女女女"; } x = (x+1)%2; } } } //輸出 class Output implements Runnable { Resource r; Output(Resource r) { this.r = r; } public void run() { while(true) { System.out.println(r.name+"....."+r.sex); } } } class ResourceDemo { public static void main(String[] args) { //建立資源。 Resource r = new Resource(); //建立任務。 Input in = new Input(r); Output out = new Output(r); //建立執行緒,執行路徑。 Thread t1 = new Thread(in); Thread t2 = new Thread(out); //開啟執行緒 t1.start(); t2.start(); } }
執行結果如下圖所示,產生了多執行緒的安全問題,因為有多個執行緒在操作同一個共享資料(name,sex)。當輸入方法還未執行完畢,輸出方法就搶先將還未賦值正確的(name,sex)進行輸出了。 改進辦法:加入同步鎖(監視器) 由於使用的都是r物件這同一個鎖,可以保證輸入方法執行完畢之後,另一個執行緒再執行輸出方法。 上圖中,輸出方法拿到執行權後進行了反覆輸出,能否做到只輸出一次,就讓輸入方法進行修改的效果呢?下面的機制可以解決。
等待/喚醒機制
1,wait(): 讓執行緒處於凍結狀態,被wait的執行緒會被儲存到執行緒池中。 如果物件呼叫了wait方法就會使持有該物件的執行緒把該物件的控制權交出去,然後處於等待狀態。
2,notify():喚醒執行緒池中一個執行緒(任意)。如果物件呼叫了notify方法就會通知某個正在等待這個物件的控制權的執行緒可以繼續執行。
3,notifyAll():喚醒執行緒池中的所有執行緒。如果物件呼叫了notifyAll方法就會通知所有等待這個物件控制權的執行緒繼續執行。
這些方法都必須定義在同步中。 因為這些方法是用於操作執行緒狀態的方法。 必須要明確到底操作的是哪個鎖上的執行緒。
使用wait方法和使用synchornized來分配cpu時間是有本質區別的。wait會釋放鎖,synchornized不釋放鎖。
以下例子對上面的程式碼進行了優化,使用wait方法和notify方法,使兩個執行緒交替進行。
class Resource
{
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name,String sex)
{
if(flag)
try{this.wait();}catch(InterruptedException e){} //同步函式的鎖是this
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out()
{
if(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(name+"...+...."+sex);
flag = false;
notify();
}
}
//輸入
class Input implements Runnable
{
Resource r ;
Input(Resource r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
if(x==0)
{
r.set("mike","nan");
}
else
{
r.set("麗麗","女女女女女女");
}
x = (x+1)%2;
}
}
}
//輸出
class Output implements Runnable
{
Resource r;
Output(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ResourceDemo3
{
public static void main(String[] args)
{
//建立資源。
Resource r = new Resource();
//建立任務。
Input in = new Input(r);
Output out = new Output(r);
//建立執行緒,執行路徑。
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//開啟執行緒
t1.start();
t2.start();
}
}
多生產者+多消費者問題
這是一個經典例子,程式碼如下:
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)// t0 t1
{
while(flag)
try{this.wait();}catch(InterruptedException e){} //若執行緒在此處被喚醒,需要回頭重新判斷標記
this.name = name + count;//烤鴨1 烤鴨2 烤鴨3
count++;//2 3 4
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);//生產烤鴨1 生產烤鴨2 生產烤鴨3
flag = true;
notify();
}
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(InterruptedException e){} //t2 t3
System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1
flag = false;
notify();
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.set("烤鴨");
}
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
while判斷標記,解決了執行緒獲取執行權後,是否要執行的判斷問題 notify:只能喚醒一個執行緒,如果本方喚醒了本方,沒有意義。而且while判斷標記+notify會導致死鎖(如下圖)。因為所有執行緒都被凍結無人喚醒。
notifyAll達到了這樣的效果:本方執行緒一定會喚醒對方執行緒。 ——小結:while+notifyAll可以解決此類問題。 那麼問題來了,能否有更好的方法呢?
介面 Lock
jdk1.5以後將同步和鎖封裝成了物件,並將操作鎖的隱式方式定義到了該物件中,將隱式動作變成了顯示動作。 以下引用自API文件:
Lock 實現提供了比使用 synchronized方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支援多個相關的 Condition 物件。
鎖是控制多個執行緒對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個執行緒獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。不過,某些鎖可能允許對共享資源併發訪問, ReadWriteLock 的讀取鎖。
synchronized方法或語句的使用提供了對與每個物件相關的隱式監視器鎖的訪問,但卻強制所有鎖獲取和釋放均要出現在一個塊結構中:當獲取了多個鎖時,它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的詞法範圍內釋放所有鎖。
雖然 synchronized 方法和語句的範圍機制使得使用監視器鎖程式設計方便了很多,而且還幫助避免了很多涉及到鎖的常見程式設計錯誤,但有時也需要以更為靈活的方式使用鎖。例如,某些遍歷併發訪問的資料結果的演算法要求使用 “hand-over-hand” 或 “chain locking”:獲取節點 A 的鎖,然後再獲取節點 B 的鎖,然後釋放 A 並獲取 C,然後釋放 B 並獲取 D,依此類推。
Lock介面的實現允許鎖在不同的作用範圍內獲取和釋放,並允許以任何順序獲取和釋放多個鎖,從而支援使用這種技術。
隨著靈活性的增加,也帶來了更多的責任。不使用塊結構鎖就失去了使用 synchronized 方法和語句時會出現的鎖自動釋放功能。在大多數情況下,應該使用以下語句:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
Lock介面: 出現替代了同步程式碼塊或者同步函式。將同步的隱式鎖操作變成現實鎖操作。 同時更為靈活。可以一個鎖上加上多組監視器。 lock():獲取鎖。 unlock():釋放鎖,通常需要定義finally程式碼塊中。
Condition介面:出現替代了Object中的wait notify notifyAll方法。 將這些監視器方法單獨進行了封裝,變成Condition監視器物件。 可以任意鎖進行組合。 await(); signal(); signalAll(); 將其應用到上面提到的生產者消費者問題中:
import java.util.concurrent.locks.*;
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
// 建立一個鎖物件。
Lock lock = new ReentrantLock();
//通過已有的鎖獲取該鎖上的監視器物件。
// Condition con = lock.newCondition();
//通過已有的鎖獲取兩組監視器,一組監視生產者,一組監視消費者。
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void set(String name)// t0 t1
{
lock.lock();
try
{
while(flag)
// try{lock.wait();}catch(InterruptedException e){}// 對比,這是之前的寫法
try{producer_con.await();}catch(InterruptedException e){}
this.name = name + count;//烤鴨1 烤鴨2 烤鴨3
count++;//2 3 4
System.out.println(Thread.currentThread().getName()+"...生產者5.0..."+this.name);//生產烤鴨1 生產烤鴨2 生產烤鴨3
flag = true;
consumer_con.signal(); //喚醒對方執行緒
}
finally
{
lock.unlock(); //釋放鎖
}
}
public void out()// t2 t3
{
lock.lock();
try
{
while(!flag)
// try{this.wait();}catch(InterruptedException e){} //t2 t3
try{cousumer_con.await();}catch(InterruptedException e){} //t2 t3
System.out.println(Thread.currentThread().getName()+"...消費者.5.0......."+this.name);//消費烤鴨1
flag = false;
producer_con.signal();
}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.set("烤鴨");
}
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
wait 和 sleep 的區別
1,wait可以指定時間也可以不指定。 sleep必須指定時間。
2,在同步中時,對cpu的執行權和鎖的處理不同。 wait:釋放執行權,釋放鎖。 sleep:釋放執行權,不釋放鎖。
停止執行緒
停止執行緒: 1,stop方法。
2,run方法結束。
怎麼控制執行緒的任務結束呢? 任務中都會有迴圈結構,只要控制住迴圈就可以結束任務。 控制迴圈通常就用定義標記來完成。
但是如果執行緒處於了凍結狀態,無法讀取標記。如何結束呢? 可以使用interrupt()方法將執行緒從凍結狀態強制恢復到執行狀態中來,讓執行緒具備cpu的執行資格。 但是強制動作會發生了InterruptedException,記得要處理。
執行緒類的其他方法
守護執行緒:setDaemon(boolean on) 將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。 該方法必須在啟動執行緒前呼叫。 join
public final void join()
throws InterruptedException
等待當前執行緒終止(就加入)。 臨時加入一個執行緒運算時可以使用join方法。
多執行緒總結
1,程序和執行緒的概念。 |–程序: |–執行緒:
2,jvm中的多執行緒體現。 |–主執行緒,垃圾回收執行緒,自定義執行緒。以及他們執行的程式碼的位置。
3,什麼時候使用多執行緒,多執行緒的好處是什麼?建立執行緒的目的? |–當需要多部分程式碼同時執行的時候,可以使用。
4,建立執行緒的兩種方式。★★★★★ |–繼承Thread |–步驟 |–實現Runnable |–步驟 |–兩種方式的區別?
5,執行緒的5種狀態。 對於執行資格和執行權在狀態中的具體特點。 |–被建立: |–執行: |–凍結: |–臨時阻塞: |–消亡:
6,執行緒的安全問題。★★★★★ |–安全問題的原因: |–解決的思想: |–解決的體現:synchronized |–同步的前提:但是加上同步還出現安全問題,就需要用前提來思考。 |–同步的兩種表現方法和區別: |–同步的好處和弊端: |–單例的懶漢式。 |–死鎖。
7,執行緒間的通訊。等待/喚醒機制。 |–概念:多個執行緒,不同任務,處理同一資源。 |–等待喚醒機制。使用了鎖上的 wait notify notifyAll. ★★★★★ |–生產者/消費者的問題。並多生產和多消費的問題。 while判斷標記。用notifyAll喚醒對方。 ★★★★★ |–JDK1.5以後出現了更好的方案,★★★ Lock介面替代了synchronized Condition介面替代了Object中的監視方法,並將監視器方法封裝成了Condition 和以前不同的是,以前一個鎖上只能有一組監視器方法。現在,一個Lock鎖上可以多組監視器方法物件。 可以實現一組負責生產者,一組負責消費者。 |–wait和sleep的區別。★★★★★
8,停止執行緒的方式。 |–原理: |–表現:–中斷。
9,執行緒常見的一些方法。 |–setDaemon() |–join(); |–優先順序 |–yield(); |–在開發時,可以使用匿名內部類來完成區域性的路徑開闢。