Java基礎14--多執行緒
14-1,多執行緒-執行緒間通訊示例
1,什麼是多執行緒間通訊?
多執行緒通訊:多個執行緒在處理同一個資源,但是任務不同。
比如說:有一堆煤(資源),一輛卡車向裡放煤(Input),一輛卡車向外取煤(output),放煤和取煤的任務不同,但操作的是同一個資源。
由於有兩個任務,所以要定義兩個run方法。
2,以下面程式碼說明。
定義name和sex變數,實現交替輸出不同資訊的功能。定義兩個run方法,一個輸入名字,另一個列印名字,因為name和sex是資源,將之封裝成一個類,兩個run方法分別建立一個類,程式碼如下:
//資源 class Resource { String name; String sex; } class Input implements Runnable { Resource r; //資源是一開始就有的,定義在構造器中 Input(Resource r) { this.r = r; } public void run() { int x = 0; while(true) { //這裡會出現安全問題 synchronized(r) { if(x == 0) { r.name = "mike"; r.sex = "nan"; } else { r.name = "麗麗"; r.sex = "女女女女女"; } } //實現x在0和1之間的變化,保證兩個賦值都能執行到 x = (x + 1) % 2; } } } class Output implements Runnable { Resource r; Output(Resource r) { this.r = r; } public void run() { while(true) { synchronized(r) { //這句是把賦的值輸出出來,跟賦值有關,也應該放在同步程式碼塊中 System.out.println(r.name + "..." + r.sex); } } } } class ResourceDemo { public static void main(String[] args) { //建立資源 /* 建立資源,並用Input和Output傳入進去,保證兩個run操作的是同一個Resource物件。 並且保證了兩個執行緒用的是同一個鎖。 */ 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(); } }
說明:不加同步時可能會出現mike...女女女女女,或,麗麗...nan的情況,因為在進行完一次賦值後,切換到另一種賦值時,如賦了mike...nan,在要賦麗麗,女女女女女時,CPU在只賦了麗麗後,就切走了,這時sex還是nan,所以出現了t2輸出麗麗...nan的情況,加入同步程式碼塊就能解決。
鎖的問題:如果兩個執行緒不用同一個鎖是不能解決問題的,所以在main中建立一個Resource物件,把r傳入,這樣兩個執行緒就能用同一個鎖了。
14-2,執行緒間通訊,等待喚醒機制
1,上一個例子中,我們希望,輸出完mike...nan後就輸出麗麗...女女女女女,再輸出mike...nan這樣。
2,等待,喚醒機制
涉及的方法:
(1)wait():讓執行緒處於凍結狀態,被wait的執行緒會被儲存線上程池中。
(2)notify():喚醒執行緒池中的一個執行緒(任意)。
(3)notifyAll():喚醒執行緒池中的所有執行緒。
這些方法都必須被定義在同步中,因為這些方法是用於操作執行緒狀態的方法,必須要明確操作的是哪個鎖上的執行緒。
3,為什麼操作執行緒的方法wait,notify,notifyAll定義在了Object中?
因為這些方法是監視器的方法(鎖),監視器其實就是鎖,鎖可以是任意物件,任意物件呼叫的方法一定定義在Object中。
4,以程式碼說明14-1中程式實現1中的需求。
//資源
class Resource {
String name;
String sex;
//用於判斷name和sex的值是否為空
boolean flag = false;
}
class Input implements Runnable {
Resource r;
//資源是一開始就有的,定義在構造器中
Input(Resource r) {
this.r = r;
}
public void run() {
int x = 0;
while(true) {
//這裡會出現安全問題
synchronized(r) {
//第一次為false,不執行
if(r.flag) {
try{
r.wait();
} catch(InterruptedException e) {}
}
if(x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "麗麗";
r.sex = "女女女女女";
}
//name和sex已經賦值,不為空,置為true,t2可以從中取值輸出
r.flag = true;
//喚醒t2執行緒
r.notify();
}
//實現x在0和1之間的變化,保證兩個賦值都能執行到
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized(r) {
//flag已經是true,!flag是false,第一次不執行
if(!r.flag) {
try{
r.wait();
}catch(InterruptedException e) {}
}
//這句是把賦的值輸出出來,跟賦值有關,也應該放在同步程式碼塊中
System.out.println(r.name + "..." + r.sex);
//輸出一次把flag置為flase,防止繼續輸出mike...nan,
//因為若t2繼續拿著執行權,!flag為true,t2會被wait。
r.flag = false;
r.notify();
}
}
}
}
class ResourceDemo {
public static void main(String[] args) {
//建立資源
/*
建立資源,並用Input和Output傳入進去,保證兩個run操作的是同一個Resource物件。
並且保證了兩個執行緒用的是同一個鎖。
*/
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();
}
}
說明:第一次執行:r.flag為false,不被wait,進行賦值,mike,nan,再把flag置為true,若此時t1繼續拿著執行權,判斷if(r.flag)為true,執行r.wait,t1被凍結,同時喚醒了t2(t2不被凍結也可以被喚醒),這時只能執行t2,這時if(!r.flag)為false,不執行,執行輸出mike...nan,再把flag置為false,並喚醒t1,這時就算t2拿著執行權,if(!r.flag)為true,t2會被wait,只能執行了t1,t1這時x=1,賦值為麗麗,女女女女女,這時在重複上面的步驟,實現了1中的需求。
14-3,執行緒間通訊-等待喚醒機制-程式碼優化
上例的程式碼優化:
class Resource {
//為了保護成員變數,使其私有化,並提供public方法將其對外公開
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.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) {
//呼叫set方法賦值
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 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();
}
}
14-4,執行緒間通訊-多生產者多消費者問題
程式碼說明:
生產者,消費者,比如說生產烤鴨,消費烤鴨。
單一的生產者和消費者用上例的模式就可以解決,若是多個生產者和多個消費者,則要用下列程式解決。
/*
if判斷標記,只有一次,會導致不該執行的執行緒運行了,出現了資料錯誤的情況。
while判斷標記,解決了執行緒獲取執行權後,是否要執行。
notify只能喚醒一個執行緒,如果本方喚醒了本方,沒有意義。而且while判斷標記+notify會導致死鎖。
notifyAll解決了本方執行緒一定會喚醒對方執行緒的問題。
*/
/*
問題描述:有多個烤鴨店和多個消費者,任何一個烤鴨店生產好一隻烤鴨,其中一個消費者就會消費烤鴨。
*/
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name) {
/*
如果這裡用if,t0如果掛在try上,再被喚醒將不再進行if判斷,若為while,t0如果掛在try上,
再被喚醒將繼續判斷是否成立。
*/
while (flag) {
try {
this.wait();
} catch(InterruptedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "..生產者.." + this.name);
flag = true;
/*
notify的情況下,若t1,t2,t3都掛了,t0有執行權,那麼他可能喚醒t1,這時只有生產者沒有消費者。
*/
notifyAll();
}
public synchronized void out() {
while(!flag) {
try {
this.wait();
}catch(InterruptedException e) {}
}
System.out.println(Thread.currentThread().getName() + "..消費者.." + this.name);
flag = false;
notifyAll();
}
}
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 Sesource 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();
}
}
說明:
if和notify的情況:
to拿到執行權,if(flag)為假,生產烤鴨1,count為2,置flag為true,notify一次,若t0繼續拿執行權,if(flag)為true,被wait,t1執行,if(flag)為true,被wait,t2執行,if(!flag)為假,消費了烤鴨1,置flag為false,喚醒t0,t1中的一個,假設t0被喚醒,t3執行,if(!flag)為true,被wait(),t2執行,if(!flag)為true,也被wait(),這時活著的執行緒只有t0,t0執行,此時不再判斷if,生產了烤鴨2,count為3,flag為true,notify隨機喚醒一個,若喚醒了t1,t1也不判斷if,生產了烤鴨3,若喚醒t2,t2消費了烤鴨3,這時烤鴨2未被消費,此時烤鴨2沒有消費者,就出現了生產出的烤鴨無人消費的問題。
結果列印如:
生產烤鴨1
...消費烤鴨1
生產烤鴨2 //烤鴨2沒有被消費
生產烤鴨3
...消費烤鴨3
while和notify的情況:
繼續上面的說法,此時t1,t2,t3被wait,只有t0活著,flag為false,t0被喚醒後,需要判斷while(flag),為假,生產烤鴨4,置flag為true,t0喚醒t1,t0繼續執行,while(flag)為true,t0被wait,t1被喚醒後也判斷while(flag)為true,被wait,這時4個執行緒都被wait了,造成死鎖。
While和notifyAll解決了問題,但效率降低了,為此,JDK1.5提供瞭解決方案。
14-5,多生產者多消費者問題-JDK1.5性特性-Lock
同步程式碼塊對於鎖的操作時隱式的。
JDK1.5以後將同步和鎖封裝成了物件,並將操作鎖的隱式方式定義到了該物件中,將隱式動作變成了顯示動作。
Lock介面在java.util.concurrent.locks包中,用Lock需要匯入java.util.concurrent.locks.*;
標準寫法:
//Lock是介面,ReentrantLock是Lock的子類,實現了Lock
Lock lock = new ReentrantLock();
public void show() {
lock.lock();
try {
//程式碼中可能會丟擲異常,用try處理,丟擲異常後會導致程式跳轉,這樣就不能釋放鎖了
//釋放鎖是必須執行的,所以放在finally內。
code...
}finally {
//釋放鎖
lock.unlock();
}
}
14-6,JDK1.5新特性-Condition
1,Lock介面:替代了同步程式碼或者同步函式,將同步的隱式鎖操作變成顯示所操作,同時更為靈活,可以在一個鎖上加多組監視器。
lock()方法獲取鎖。
unlock()方法釋放鎖,通常定義在finally程式碼塊中。
2,Condition介面:替代了Object中的wait(),notify(),notifyAll()方法,將這些監視器方法單獨進行了封裝,變成Condition監視器物件,可以與任意鎖進行組合。
await():-->wait();
signal();-->notify();
signalAll();-->notifyAll();
3,實現機制對比:
舊方法:
class Object {
wait();
notify();
notifyAll();
}
class Demo extends Object {}
...
Demo d = new Demo();
synchronized(d){
d.wait();
}
一個鎖只能實現一組wait,notify,notifyAll方法。
JDK1.5新特性:
interface Condition {
await();
signal();
signalAll();
}
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
可以建立多個Condition物件,實現多組Condition中的await,signal,signalAll。
4,14-4中的程式碼可以寫為:
//建立一個鎖物件
Lock lock = new ReentrantLock();
//通過已有的鎖獲取該鎖上的監視器物件
Condition con = lock.newCondition();
public voidset(String name) {
//獲取鎖
lock.lock();
try {
while(true) {
try {
//呼叫監視器的await方法
con.awati();
}catch(InterrputedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"生產者5.0"+this.name);
flag = true;
//呼叫監視器的signalAll方法
con.signalAll();
} finally {
//釋放鎖
lock.unlock();
}
}
14-7,解決方案
1,在14-4的程式中的Resource類改為。
import java.util.concurrent.locks.*;
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
//建立鎖物件
Lock lock = new ReentrantLock();
//通過已經有的鎖獲取兩組監視器,一組監視生產者,一組監視消費者
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void set(String name) {
lock.lock();
try {
while(true) {
try {
producer_con.await();
}catch(InterruputedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName());
flag = true;
//用消費者監視器喚醒一個消費者執行緒
consumer_con.signal();
}finally {
lock.unlock();
}
}
public void out() {
lock.lock();
try{
while(!flag) {
try {
consumer_con.swait();
}catch(InterruptedException e) {}
}
flag = false;
//用生產者監視器喚醒一個生產者執行緒
producer_con.signal();
}finally {
lock.unlock();
}
}
}
2,圖示
以前的鎖:三個方法能操作執行緒池中的所有執行緒,但只有一組監聽器。
現在的鎖:三個方法分別操作兩個監聽器中的物件。
14-8,JDK1.5解決方法—範例
這是JDK API文件中Condition介面中給出的範例:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
14-9,多執行緒-wait和sleep的區別
1,wait和sleep的區別?
(1)wait可以指定時間也可以不指定。
sleep必須執行時間。
(2)在同步中時,對CPU的執行權和鎖的處理不同。
wait:釋放執行權,釋放鎖。
sleep:釋放執行權,不釋放鎖。
2,示例:
class Demo {
void show() {
synchronized(this) {
wait();//t0,t1,t2都掛在這裡
}
}
void method() {
synchronized(this) { //t4拿執行權
notifyAll();//喚醒全部,t0,t1,t2
}//t4結束
}
}
這時t0,t1,t2都已經進入到同步內,t0,t1,t2都有執行資格,但t4釋放鎖後,只有一個執行緒拿到鎖,所以還能保證同步性。
14-10,停止執行緒的方式-定義標記
1,停止執行緒
(1)stop方法(已過時,不可用)
(2)run方法結束。
如何控制執行緒的任務結束呢?
任務中一般都會有迴圈結構,只要控制住迴圈就可以結束任務,控制迴圈結束通常以定義標記的形式完成。如:
class StopThread implements Runnable {
private boolean flag = true;
public void run() {
while(flag) {
System.out.println(Thread.currentThread().getName()+"....");
}
}
public void setFlag() {
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 1;
for(;;) {
//定義迴圈結束條件
if(++num == 50) {
/*
如果num=50,則把flag置為false,並退出無限迴圈。
將flag置為false,則run方法中的while(flag)為假,不再執行。
實現了用標記結束run方法。
*/
st.setFlag();
break;
}
System.out.println("main..." + num);
}
System.out.println("over");
}
}
14-11,停止執行緒的方式-Interrupt
1,如果執行緒處於了凍結狀態,無法讀取標記,該如何結束呢?
可以使用Interrupt方法將執行緒從凍結狀態強制恢復到執行狀態中來,讓執行緒具備CPU的執行資格。Interrupt方法為強制動作,會發生InterruptedException異常,記得要處理。
例如:
class StopThread implements Runnable {
private boolean flag = true;
public synchronized void run() {
while(flag) {
//產生InterruptException異常,用try-catch處理
try {
//t1,t2進入後會被wait,用後面的interrupt方法中斷wait,
//是t1,t2可以繼續執行輸出,並把標記改為false,是執行緒結束。
wait();
}catch(InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"..."+e);
flag = false;
}
System.out.println(Thread.currentThread().getName() + ".....");
}
}
public void setFlag() {
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 1;
for(;;) {
if(++num == 50) {
//interrupt方法中斷t1,t2的wait狀態。
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("main ... " + num);
}
System.out.println("over");
}
}
14-12,守護執行緒-setDeamon
1,守護執行緒setDeamon:將該執行緒標記為守護或使用者執行緒,當正在執行的執行緒都是守護執行緒時,Java虛擬機器退出。
可將守護執行緒理解為後臺執行緒,後臺執行緒依附於前臺執行緒,前臺執行緒結束後,後臺執行緒也自動結束。
2,如上例中,把t2.interrupt()註釋掉,這樣只有t1繼續執行並且t1執行緒結束,t2結束不了,則整個程序結束不了,若在t2.start()上方加上一句:t2.setDeamon(),則t2變為後臺執行緒,當主執行緒與t1執行緒都結束時,t2也會隨之結束。
setDeamon方法必須在啟動執行緒前呼叫。
14-13,其他方法-join等
1,join方法:等待該執行緒結束。
如:
t1.start();
t1.join(); //從main得到執行權,等到t1執行完,t2和main在執行。
t2.start();
若把t1.join()放在t2.start()下面,則main不執行,t2和t1隨機執行,main只等t1結束後就開始執行,跟t2沒有關係。
什麼時候用join?
在臨時加入一個執行緒運算時可以使用join方法。
2,優先順序
Thread類中有toString()方法,返回執行緒名字,優先順序和執行緒組。
執行緒的優先順序是指執行緒被CPU執行的機率,值越高,機率越大,範圍是1-10。
Thread中有三個欄位:
staticint MAX_PRIORITY;值為10
staticint MIN_PRIORITY;值為1
staticint NORM_PRIORITY;值為5
如:
將t1的優先順序設定為10可以這麼寫:
t1.setPriority(Thread.MAX_PRIORITY);
3,執行緒組:把執行緒進行組的劃分。
若要對一組執行緒進行某種統一的操作,可將這組執行緒加入執行緒組(ThreadGroup)。
4,yield()方法,臨時暫停執行緒使用。