1. 程式人生 > >執行緒間通訊、等待喚醒機制、生產者消費者問題(Lock,Condition)、停止執行緒和守護執行緒、執行緒優先順序

執行緒間通訊、等待喚醒機制、生產者消費者問題(Lock,Condition)、停止執行緒和守護執行緒、執行緒優先順序

1  執行緒間通訊

1.1  執行緒間通訊

其實就是多個執行緒在操作同一個資源,但是操作的動作不同。

比如一個執行緒給一個變數賦值,而另一個執行緒列印這個變數。

1.2  等待喚醒機制

wait()將執行緒等待,釋放了CPU執行權,同時將執行緒物件儲存到執行緒池中。

notify():喚醒執行緒池中一個等待的執行緒,若執行緒池有多個等待的執行緒,則任意喚醒一個。

notifyAll():喚醒執行緒池中,所有的等待中的執行緒。

這三個方法都要使用在同步中,因為要對持有鎖的執行緒進行操作。

比如,A鎖上的執行緒被wait了,那麼這個執行緒就進入了A鎖的執行緒池中,只能被A鎖的notify喚醒,而不能被不同鎖的其他執行緒喚醒。

所以這三個方法要使用在同步中,因為只有同步才具有鎖。

而鎖可以是任意物件,這三個方法被鎖呼叫,所以這三個方法可以被任意物件呼叫,所以這三個方法定義在Object類中。

wait()sleep()的區別:

wait():可以指定等待的時間,也可以不指定時間,如果不指定時間,就只能被同一個鎖的notifynotifyAll喚醒。wait時執行緒會釋放CPU執行權,並且會釋放鎖。

sleep():必須指定執行緒休眠的時間,執行緒休眠即暫停執行。時間到了,執行緒就自動甦醒,恢復執行。sleep時執行緒會釋放執行權,但不釋放鎖。

執行緒的停止:

1,如果run()方法中定義了迴圈,可以用迴圈結束標記,跳出迴圈,則執行緒就停止了。

 

2,如果執行緒已被凍結,讀不到迴圈結束標記,則需要通過Thread類的interrupt方法中斷執行緒,讓執行緒重新獲得執行的資格,從而可以讀到迴圈結束標記,而結束執行緒。

3setDaemon(true)方法將當前執行緒標記為守護執行緒,當執行的執行緒都是守護執行緒時,則Java虛擬機器退出。該方法必須在啟動執行緒前呼叫。

等待喚醒機制程式碼,實現兩個執行緒交替執行,在控制檯上交替列印兩個字串。

等待和喚醒是同一個鎖r

//兩個執行緒交替執行,在控制檯交替列印兩串字串。
class Res{
	String name;
	String sex;
	boolean flag = false; //等待喚醒機制
}
class Input implements Runnable{
	private Res r;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			synchronized(r){  //等待和喚醒,是同一個鎖。
				if(r.flag)    //等待喚醒機制,true則等待,false則執行
					try{r.wait();}catch(Exception e){}  //執行緒等待,進入執行緒池
				if(x == 0){
					r.name = "LuoQi";
					r.sex = "man";
				}
				else{
					r.name = "麗麗";  //賦值時,賦值了name還沒賦值sex,就列印“lili----male”,加同步鎖,牢記同步前提。
					r.sex = "女";
				}
				x = (x+1)%2;
				r.flag = true;  //等待喚醒機制
				r.notify();  //任意喚醒執行緒池裡的一個被等待的執行緒  //等待喚醒機制
			}
		}
	}
}
class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			synchronized(r){  //等待和喚醒,是同一個鎖。
				if(!r.flag)   //等待喚醒機制,false則等待,true則執行
					try{r.wait();}catch(Exception e){}  //執行緒等待,進入執行緒池
				System.out.println(r.name+"----"+r.sex);
				r.flag = false;  //等待喚醒機制
				r.notify();  //喚醒Input執行緒  //等待喚醒機制
			}
		}
	}
}
class ThreadCommunication{
	public static void main(String[] args){
		Res r = new Res();
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

以上程式碼中,把兩個同步程式碼塊中的程式碼,封裝成兩個同步方法,一個更改兩個欄位的值,另一個列印兩個欄位的值。

兩個同步方法寫在Res類中,這樣同步鎖都是Res.class位元組碼檔案,保證了等待和喚醒是同一個鎖:

//兩個執行緒交替執行,在控制檯交替列印兩串字串。
class Res{
	String name;
	String sex;
	boolean flag = false; 
	
	public synchronized void setRes(String name,String sex){ //同步函式
		if(this.flag)    //flag為true,則執行緒等待進入執行緒池。
			try{this.wait();}catch(Exception e){} 
		this.name = name;  //flag為false,則執行緒繼續執行。
		this .sex = sex;
		this.flag = true;
		this.notify();   //任意喚醒執行緒池中一個等待的執行緒
	}
	public synchronized void getRes(){
		if(!this.flag)  //flag為false,則執行緒等待進入執行緒池。
			try{this.wait();}catch(Exception e){} 
		System.out.println(this.name+"----"+this.sex); //flag為true則繼續執行
		this.flag = false;
		this.notify();  //任意喚醒執行緒池中一個等待的執行緒
	}
}
class Input implements Runnable{
	private Res r;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			if(x == 0)
				r.setRes("LuoQi","man");
			else
				r.setRes("麗麗","女");
			x = (x+1)%2;	
		}
	}
}
class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			r.getRes();
		}
	}
}
class ThreadCommunication2{
	public static void main(String[] args){
		Res r = new Res();
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

2  生產者消費者問題

2.1  JDK1.5以前

使用執行緒間通訊和執行緒同步解決生產者消費者問題。

while迴圈判斷標記和notifyAll()

當出現多個生產者和多個消費者時,必須用while迴圈判斷標記,和notifyAll喚醒全部執行緒。

對於多個生產者和消費者,為什麼要定義while判斷標記?

原因:讓被喚醒的執行緒再一次判斷標記。

為什麼使用notifyAll()

因為需要喚醒對方執行緒,因為notify是隨機喚醒一個執行緒,容易出現只喚醒本方執行緒的情況,導致程式中的所有執行緒都等待。

程式碼和註釋:

package mypkg;
class ProducerConsumerDemo{
	public static void main(String[] args){
		Resource r = new Resource();
		
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		
		Thread t1 = new Thread(pro);  //兩個生產者執行緒
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);  //兩個消費者執行緒
		Thread t4 = new Thread(con);
		t1.start();
		t2.start();
		t3.start();
		t4.start(); 
	}
}
class Resource{
	private String name;
	private int count = 1;
	private boolean flag = false;
	
	public synchronized void set(String name){  //t1  t2
		while(this.flag)           //while迴圈判斷標記,讓被喚醒的執行緒再次判斷標記。標記為true則執行緒等待,為false則執行緒繼續執行
			try{this.wait();} catch(Exception e){}
		this.name = name+"--"+count++;
		System.out.println(Thread.currentThread().getName()+"---生產者---"+this.name);
		this.flag = true;
		this.notifyAll();   //必須喚醒對方,索性喚醒全部。因為有可能生產者喚醒了生產者,導致有的商品被生產了但沒被消費。
	}
	public synchronized void get(){  //t3  t4
		while(!this.flag)
			try{this.wait();} catch(Exception e){}			
		System.out.println(Thread.currentThread().getName()+"---消費者---------"+this.name);
		this.flag = false;
		this.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 Resource r;
	Consumer(Resource r){
		this.r = r;
	}
	public void run(){
		while(true){
			r.get();
		}
	}
}

執行結果:


2.2  JDK1.5以後

JDK1.5 中提供了執行緒同步和執行緒間通訊的升級解決方案,執行緒同步、執行緒間通訊和等待喚醒機制都有了變化。

1,將同步Synchronized替換成顯式的Lock操作。

2,將同步鎖繼承自Object類的wait()notify()notifyAll()操作,替換成了Condition物件的await()signal()signalAll()操作。

3,該Condition物件可以通過顯式的Lock鎖來建立。

顯式的鎖機制,以及顯式的鎖物件上的等待喚醒操作機制,同時把等待喚醒進行封裝。

封裝完後,一個鎖可以對應多個Condition,等待和喚醒必須是同一個Condition物件呼叫。

JDK1.5之前,等待和喚醒必須是同一個鎖呼叫;

JDK1.5之後,等待和喚醒必須是同一個Condition物件呼叫,而一個Lock鎖可以建立多個Condition物件。

從而,可以在生產者執行緒中,只喚醒消費者的等待執行緒,即呼叫消費者的Condition物件的喚醒操作。

Lock介面,它的一個子類是ReentrantLock,建立物件時new一個ReentrantLock物件。

ReentrantLock類的常用方法:

newCondition():建立鎖LockCondition物件,用來呼叫操作。 

lock():獲取鎖。

unlock():釋放此鎖。

Condition類的常用方法:

await(): 執行緒進入等待狀態,並丟擲一個InterruptedException異常。

signal(): 喚醒一個等待執行緒。

signalAll(): 喚醒所有等待執行緒。

import java.util.concurrent.locks.*; 
class ProducerConsumerDemo2{
	public static void main(String[] args){
		Resource r = new Resource();
		
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);
		t1.start();
		t2.start();
		t3.start();
		t4.start(); 
	}
}
class Resource{
	private String name;
	private int count = 1;
	private boolean flag = false;
	
	final Lock lock = new ReentrantLock();  //建立一個鎖
	final Condition condition_pro = lock.newCondition(); //建立鎖lock的Condition物件,用來操作生產者執行緒
	final Condition condition_con = lock.newCondition(); //建立鎖lock的Condition物件,用來操作消費者執行緒
	
	public void set(String name) throws InterruptedException {  //t1  t2
		lock.lock();
		try{
			while(this.flag)          
				condition_pro.await(); //await():執行緒等待,會丟擲一個異常
			this.name = name+"--"+count++;
			System.out.println(Thread.currentThread().getName()+"---生產者---"+this.name);
			this.flag = true;
			condition_con.signal();   //生產者中喚醒消費者
		}
		finally{
			lock.unlock();  //釋放鎖的動作一定要執行,所以在finally中
		}
	}
	public void get() throws InterruptedException {  //t3  t4
		lock.lock();
		try{
			while(!this.flag)
				condition_con.await();		
			System.out.println(Thread.currentThread().getName()+"---消費者---------"+this.name);
			this.flag = false;
			condition_pro.signal();  //消費者中喚醒生產者
		}
		finally{
			lock.unlock();
		}
	}
}
class Producer implements Runnable{
	private Resource r;
	Producer(Resource r){
		this.r = r;
	}
	public void run(){
		while(true){
			try{
				r.set("+商品+");
			}
			catch(InterruptedException e){}
		}
	}
}
class Consumer implements Runnable{
	private Resource r;
	Consumer(Resource r){
		this.r = r;
	}
	public void run(){
		while(true){
			try{
				r.get();
			}
			catch(InterruptedException e){}
		} 
	}
}

3  停止執行緒和守護執行緒

3.1  停止執行緒

以前可以使用stop方法來停止執行緒,但是已經過時,那現在如何停止執行緒?

只有一種方法:run方法結束。

開啟多執行緒執行,run方法內的執行程式碼通常是迴圈結構,

只要控制住迴圈,就可以讓run方法結束,也就是執行緒結束。

特殊情況:

當執行緒處於了凍結狀態,就不會讀取到標記,那麼執行緒就不會結束。

當沒有指定的方式讓凍結的執行緒恢復到執行狀態時,這是需要對凍結進行清除。

強制讓現場恢復到執行狀態中來,這樣就可以操作標記讓執行緒結束。

Thread類中提供該方法,interrupt()方法。

interrupt()方法是把執行緒從凍結狀態恢復到執行狀態。

3.2  守護執行緒

Thread類中的setDaemon方法

setDaemon(boolean on)

on如果為 true,則將該執行緒標記為守護執行緒。

守護執行緒,當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。該方法必須在啟動執行緒前呼叫。

JVM退出,守護執行緒在後臺執行,理解為後臺執行緒;全部為後臺執行緒時,由前臺轉為後臺,JVM則退出。

程式碼示例:

class StopThread implements Runnable{
	private boolean flag = true;
	public synchronized void run(){
		while(flag){
			try{
				wait();
			}
			catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"....Exception");
				flag = false;
			}
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	public void changeFlag(){
		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.setDaemon(true);  //守護執行緒,當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。該方法必須在啟動執行緒前呼叫。
		//t2.setDaemon(true);
		t1.start();
		t2.start();
		
		int num = 0;
		while(true){
			if(num++ == 60){
				//st.changeFlag();
				t1.interrupt();  //中斷執行緒,讓執行緒從凍結狀態恢復到執行狀態,這樣可以讀到flag標記從而結束執行緒。
				t2.interrupt();
				break;  //跳出while迴圈
			}
			System.out.println(Thread.currentThread().getName()+"......"+num);
		}
		System.out.println("over");
	}
}

4  執行緒的join()方法

join()

A執行緒執行到了B執行緒的join()方法時,那麼A執行緒就會等待;等B執行緒執行完,A才會執行。

join()可以用來臨時加入執行緒執行。

程式碼示例:

class Demo implements Runnable{
	public void run(){
		for(int x=0;x<70;x++){
			System.out.println(Thread.currentThread().getName()+"....."+x);
		}
	}
}
class JoinDemo{
	public static void main(String[] args) throws Exception{
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.start();
		t2.start();
		t1.join();  //t1執行緒向主執行緒索要CPU執行權,主執行緒阻塞,釋放CPU執行權,但釋放後t1和t2競爭CPU執行權;
		             //t1執行緒執行結束後,主執行緒繼續。
		
		for(int x=0; x<80; x++){
			System.out.println("main...."+x);
		}
		System.out.println("over");
	}
}

5  執行緒優先順序和yield()方法

5.1  執行緒優先順序

執行緒優先順序:

優先順序高的執行緒,爭奪CPU執行權的頻率就高,拿到CPU資源的可能性更大,

但並不是說優先順序低的執行緒就不執行了。

Thread類中定義了三個優先順序常量:

MAX_PRIORITY 值為10,為最高優先順序;

MIN_PRIORITY 值為1,為最低優先順序;

NORM_PRIORITY 值為5,預設優先順序。

新建執行緒將繼承建立它的父執行緒的優先順序,父執行緒是指執行建立新執行緒的語句所線上程,它可能是主執行緒,也可能是另一個自定義執行緒。

一般情況下,主執行緒具有預設優先順序,為5

可以通過getPriority()方法獲得執行緒的優先順序,也可以通過setPriority()方法來設定優先順序。

5.2  yield()方法

yield()方法:呼叫該方法後,可以使具有與當前執行緒相同優先順序的執行緒有執行的機會。

可以臨時暫停當前執行緒,釋放CPU執行權,讓相同優先順序的其他執行緒執行。

如果沒有相同優先順序的執行緒,那麼yield()方法什麼也不做,當前執行緒繼續執行。

程式碼示例:

class Demo implements Runnable{
	public void run(){
		for(int x=0;x<70;x++){
			System.out.println(Thread.currentThread().getName()+"....."+x);
			Thread.yield(); //t1暫停,t2執行;t2暫停,t1執行。
							 //表現為t1、t2交替執行。
		}
	}
}
class YieldDemo{
	public static void main(String[] args){
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.start();
		t2.start();
		
		for(int x=0; x<80; x++){
			//System.out.println("main...."+x);
		}
		System.out.println("over");
	}
}

6  開發中什麼時候使用多執行緒?

當某些程式碼需要同時被執行時,就用單獨的執行緒進行封裝。

比如三個for迴圈同時執行,用多執行緒,高效,程式碼示例:

class ThreadTest{   //三個for同時執行,用多執行緒,高效。
	public static void main(String[] args){
		new Thread(){  //匿名內部類
			public void run(){
				for(int x=0; x<50; x++){
					System.out.println(Thread.currentThread().getName()+"....."+x);
				}
			}
		}.start();
		
		for(int x=0; x<50; x++){
			System.out.println(Thread.currentThread().getName()+"....."+x);
		}
		
		Runnable r = new Runnable(){   //匿名內部類
			public void run(){
				for(int x=0; x<50; x++){
					System.out.println(Thread.currentThread().getName()+"....."+x);
				}		
			}
		};
		new Thread(r).start();
	}
}