執行緒間通訊、等待喚醒機制、生產者消費者問題(Lock,Condition)、停止執行緒和守護執行緒、執行緒優先順序
1 執行緒間通訊
1.1 執行緒間通訊
其實就是多個執行緒在操作同一個資源,但是操作的動作不同。
比如一個執行緒給一個變數賦值,而另一個執行緒列印這個變數。
1.2 等待喚醒機制
wait():將執行緒等待,釋放了CPU執行權,同時將執行緒物件儲存到執行緒池中。
notify():喚醒執行緒池中一個等待的執行緒,若執行緒池有多個等待的執行緒,則任意喚醒一個。
notifyAll():喚醒執行緒池中,所有的等待中的執行緒。
這三個方法都要使用在同步中,因為要對持有鎖的執行緒進行操作。
比如,A鎖上的執行緒被wait了,那麼這個執行緒就進入了A鎖的執行緒池中,只能被A鎖的notify喚醒,而不能被不同鎖的其他執行緒喚醒。
所以這三個方法要使用在同步中,因為只有同步才具有鎖。
而鎖可以是任意物件,這三個方法被鎖呼叫,所以這三個方法可以被任意物件呼叫,所以這三個方法定義在Object類中。
wait()和sleep()的區別:
wait():可以指定等待的時間,也可以不指定時間,如果不指定時間,就只能被同一個鎖的notify或notifyAll喚醒。wait時執行緒會釋放CPU執行權,並且會釋放鎖。
sleep():必須指定執行緒休眠的時間,執行緒休眠即暫停執行。時間到了,執行緒就自動甦醒,恢復執行。sleep時執行緒會釋放執行權,但不釋放鎖。
執行緒的停止:
1,如果run()方法中定義了迴圈,可以用迴圈結束標記,跳出迴圈,則執行緒就停止了。
2,如果執行緒已被凍結,讀不到迴圈結束標記,則需要通過Thread類的interrupt方法中斷執行緒,讓執行緒重新獲得執行的資格,從而可以讀到迴圈結束標記,而結束執行緒。
3,setDaemon(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():建立鎖Lock的Condition物件,用來呼叫操作。
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();
}
}