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();
}
}