1. 程式人生 > >Java基礎14--多執行緒

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()方法,臨時暫停執行緒使用。