線程間通信、等待喚醒機制、生產者消費者問題(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();
	}
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章