java執行緒高併發程式設計
權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/LiuRenyou/article/details/72805889
java執行緒詳解及高併發程式設計庖丁解牛
執行緒概述:
祖宗:
說起java高併發程式設計,就不得不提起一位老先生Doug Lea,這位老先生可不得了,看看百度百科對他的評價,一點也不為過:
如果IT的歷史,是以人為主體串接起來的話,那麼肯定少不了Doug Lea。這個鼻樑掛著眼鏡,留著德王威廉二世的鬍子,臉上永遠掛著謙遜靦腆笑容,服務於紐約州立大學Oswego分校計算機科學系的老大爺。
說他是這個世界上對Java影響力最大的個人,一點也不為過。因為兩次Java歷史上的大變革,他都間接或直接的扮演了舉足輕重的角色。一次是由JDK 1.1到JDK 1.2,JDK1.2很重要的一項新創舉就是Collections,其Collections的概念可以說承襲自Doug Lea於1995年釋出的第一個被廣泛應用的collections;一次是2004年所推出的Tiger。Tiger廣納了15項JSRs(Java Specification Requests)的語法及標準,其中一項便是JSR-166。JSR-166是來自於Doug編寫的util.concurrent包。
值得一提的是: Doug Lea也是JCP (Java社群專案)中的一員。
Doug是一個無私的人,他深知分享知識和分享蘋果是不一樣的,蘋果會越分越少,而自己的知識並不會因為給了別人就減少了,知識的分享更能激盪出不一樣的火花。《Effective JAVA》這本Java經典之作的作者Joshua Bloch便在書中特別感謝Doug Lea是此書中許多構想的共鳴板,感謝Doug Lea大方分享豐富而又寶貴的知識。
我記住了兩句話:他是這個世界上對Java影響力最大的個人和幾乎所有的java高併發程式設計核心包都是他寫的。
執行緒和程序:
程序:每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換會有較大的開銷,一個程序包含1--n個執行緒。
執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。
執行緒和程序一樣分為五個階段:建立、就緒、執行、阻塞、終止。
多程序是指作業系統能同時執行多個任務(程式)。
多執行緒是指在同一程式中有多個順序流在執行。
多執行緒的優勢:
程序之間不能共享記憶體,但執行緒可以。
系統建立程序需要為該程序重新分配系統資源,開銷大,但執行緒則小得多,所以使用多執行緒實現併發比用多程序實現併發的效能要高得多。
執行緒的建立和啟動:
繼承Thread類建立執行緒:
public class FirstThread extends Thread{
private int i;
@Override
public void run() {
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
new FirstThread().start();
new FirstThread().start();
}
}
}
}
如果希望呼叫子執行緒start()後子執行緒立馬執行,則可以在當前執行的執行緒休眠1ms;
實現Runnable介面建立執行緒:
public class SecondRunnable implements Runnable{
private int i;
@Override
public void run() {
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondRunnable sr=new SecondRunnable();
new Thread(sr,"thread-1").start();
new Thread(sr,"thread-2").start();
}
}
}
}
使用Callable和Future穿建立執行緒:
Java5開始,Java提供了Callable介面,該介面提供了一個call方法作為執行緒執行體,但是call方法比run方法更強大。call方法可以有返回值,可以拋異常。
Java5還提供了Future介面來代表Callable接口裡的call方法的返回值,併為Future介面提供了一個FutureTask實現類,這個類還實現了Runnable介面,可以作為Thread類的target,在Future接口裡定義瞭如下幾個公共方法來控制它關聯的Callable任務。
boolean cancle(boolean mayInterruptIfRunning):試圖取消Future裡關聯的Callable任務。
V get()返回Callable任務裡call方法的返回值,呼叫該方法會導致程式阻塞,必須等到子執行緒結束才會得到返回值,如圖所示可以看出。
V get(long timeout,TimeUnit unit)返回Callable任務裡call方法的返回值,該方法讓程式最多阻塞timeout和unit指定的時間,如果超時依然沒有返回值,則會丟擲TimeOutExecption。
boolean isCancelled()如果在Callable任務正常完成之前被取消,返回true。
boolean isDone()如果Callable任務已完成,則返回true。
public class ThirdFutureTask {
public static void main(String[] args) {
FutureTask<Integer>task=new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
});
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
new Thread(task,"return").start();
}
}
try {
System.out.println("子執行緒的返回值"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
建立執行緒的三種方式對比:
採用繼承Thread的方式建立執行緒優缺點:
優勢:編寫簡單,訪問當前執行緒無需Thread.currentThread()
劣勢:出於java單繼承的原因,只能繼承一個類
採用實現Runnable,Callable介面建立執行緒優缺點:
優勢:可以繼承其他類,多個執行緒可以共享同一個target,非常適合多個執行緒來處理同一份資源,體現了面向物件思想。
劣勢:編寫更復雜。要訪問當前執行緒需要Thread.currentThread()
執行緒的生命週期:
新建和就緒狀態:
當程式使用new關鍵字建立了一個執行緒後,該執行緒就處於新建狀態,此時和其它java物件一樣,僅僅由虛擬機器為其分配記憶體,初始化成員變數。
當程式物件呼叫了start後,該執行緒就處於就緒狀態,虛擬機器會為其建立方法呼叫棧和程式計數器,處於這個狀態的執行緒並沒有開始執行,只是說可以運行了,至於何時執行,取決於jvm執行緒排程器裡的排程。
執行和阻塞狀態:
如果處於就緒狀態的執行緒獲取了cpu,開始執行run()的執行緒執行體,則該執行緒處於執行狀態
如果發生以下情況,則進入阻塞狀態:
執行緒呼叫sleep(),阻塞;
執行緒呼叫了一個阻塞式的io方法,在方法返回之前,阻塞;
執行緒試圖獲得一個同步監視器,當該同步監視器正在被其它執行緒所持有,阻塞;
執行緒在等待通知notify(),阻塞;
程式呼叫suspend()將執行緒掛起,阻塞;但是該方法容易導致死鎖,儘量不用!
針對上面的幾種情況,如發生下面特定的情況可以解除阻塞,重新進入就緒狀態:
呼叫sleep方法的執行緒過了指定的時間
io方法返回
獲得同步監視器
收到通知
被掛起後resume()
執行緒死亡:
run()或call()執行完畢,死亡
執行緒丟擲一個未捕獲的異常或error,死亡
呼叫stop()容易死鎖,不建議使用。死亡
控制執行緒死亡可以用valotile的狀態標記量。
想要知道執行緒的生死,呼叫isAlive();就緒,執行,阻塞返回true,其他返回false
控制執行緒:
join執行緒:
當在某個程式執行流中呼叫其它執行緒的join方法,呼叫執行緒將阻塞,直到被join的執行緒執行完畢為止
join有三種過載方式:
join()等待被join的執行緒執行完畢
join(long millis)等待被join的執行緒最多millis毫秒,否則不再等待。
join(long millis,int nanos)等待被join的執行緒最多millis毫秒+nanos毫微秒,否則不再等待。
public class TestJoin implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
TestJoin tj=new TestJoin();
Thread thread=new Thread(tj);
thread.start();
thread.join(1);//放在start之後,不然有問題
}
}
}
}
後臺/守護執行緒:
呼叫Thread物件的setMaemon(true)設定為後臺執行緒。如果前臺執行緒都死亡,則後臺執行緒不管執沒執行完,都會死亡。
public class TestDaemonThread implements Runnable{
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
TestDaemonThread tdt=new TestDaemonThread();
Thread thread=new Thread(tdt);
thread.setDaemon(true);//設定為後臺執行緒,則前臺執行緒都死亡,這個執行緒會自動死亡,必須放在start之前
thread.start();
}
}
}
}
執行緒睡眠sleep:
讓當前執行緒暫停一段時間,進入阻塞狀態。當呼叫該方法暫停後,就緒狀態的執行緒獲得執行的機會。不理會優先順序
sleep(long millis)
sleep(long millis,int nanos)
執行緒讓步yield:
讓當前執行緒暫停一段時間,進入就緒狀態,讓系統的執行緒排程器重新排程一次,當呼叫該方法暫停後,只有優先順序大於等於當前執行緒的就緒狀態的執行緒才會獲得執行的機會。
並且該方法無需拋異常,不太建議用yield()。
改變執行緒優先順序:
Thread類提供了setPriority(int priority)來設定優先順序。引數範圍是1-10,也可以用Thread的三個靜態常量
->MAX_PRIORITY 10
->NORM_PRIORITY 5
->MIN_PRIORITY 1
執行緒同步:
執行緒安全問題:
經典例子就是取款問題,如果取款方法不是執行緒安全的,那麼當兩個執行緒同時進來取款時很有可能會發生明明餘額不足,卻把錢給取出來了的情況。
同步程式碼塊:
synchronized(account){
if(account.getBalance>=drawAmmount){
取錢成功
}else{
取錢失敗
}
}
同步方法:
public synchronized void draw(double drawAmmout){
if(balance>=drawAmmount){
取錢成功
}else{
取錢失敗
}
}
釋放同步監視器的鎖定:
釋放同步監視器的情況:
當前執行緒的同步方法,同步程式碼塊執行完畢
當前執行緒的同步方法,同步程式碼塊出現了未處理的error,exception,導致了改程式碼塊,方法異常結束
當前執行緒的同步方法,同步程式碼塊遇到了break,return終止了程式碼塊,方法的執行
執行了wait()
不會釋放同步監視器的情況:
sleep(),yield(),suspend()
同步鎖Lock:
Java5開始,java提供了功能更加強大的執行緒同步機制,通過顯示定義同步鎖物件來實現同步,由Lock物件充當同步鎖。
在實現執行緒安全的控制中,使用較多的是ReentrantLock(可重入鎖)。
package com.lry.java紮實基礎;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
static Account account=new Account("123456", 1000);
private ReentrantLock lock=new ReentrantLock();
private String accountNo;
private double money;
public Account(String accountNo, double money) {
super();
this.accountNo = accountNo;
this.money = money;
}
/**
* 取錢
* @param drawAmount 要取多少錢
*/
public void drawMoney(double drawAmount){
lock.lock();
try{
if(money>=drawAmount){
System.out.println(Thread.currentThread().getName()+"取錢成功,吐出鈔票:"+drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
money-=drawAmount;
System.out.println("賬戶餘額為"+money);
}else{
System.out.println(Thread.currentThread().getName()+"取錢失敗,鈔票不足!");
}
}finally{
lock.unlock();
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}
static class DrawThread implements Runnable{
@Override
public void run() {
account.drawMoney(600);
}
}
public static void main(String[] args) {
DrawThread dt=new DrawThread();
new Thread(dt).start();
new Thread(dt).start();
new Thread(dt).start();
new Thread(dt).start();
}
}
死鎖:
死鎖原因:
當兩個執行緒相互等待對方釋放同步監視器是發生死鎖,虛擬機器沒有監測,沒有采取措施處理死鎖,一旦出現死鎖,不會報異常,不會有提示,只會阻塞,因此自己要防止這種情況的發生。
死鎖必要條件:
從以上分析可見,如果在計算機系統中同時具備下面四個必要條件時,那麼會發生死鎖。換句話說,只要下面四個條件有一個不具備,系統就不會出現死鎖。
〈1〉互斥條件。即某個資源在一段時間內只能由一個程序佔有,不能同時被兩個或兩個以上的程序佔有。這種獨佔資源如CD-ROM驅動器,印表機等等,必須在佔有該資源的程序主動釋放它之後,其它程序才能佔有該資源。這是由資源本身的屬性所決定的。如獨木橋就是一種獨佔資源,兩方的人不能同時過橋。
〈2〉不可搶佔條件。程序所獲得的資源在未使用完畢之前,資源申請者不能強行地從資源佔有者手中奪取資源,而只能由該資源的佔有者程序自行釋放。如過獨木橋的人不能強迫對方後退,也不能非法地將對方推下橋,必須是橋上的人自己過橋後空出橋面(即主動釋放佔有資源),對方的人才能過橋。
〈3〉佔有且申請條件。程序至少已經佔有一個資源,但又申請新的資源;由於該資源已被另外程序佔有,此時該程序阻塞;但是,它在等待新資源之時,仍繼續佔用已佔有的資源。還以過獨木橋為例,甲乙兩人在橋上相遇。甲走過一段橋面(即佔有了一些資源),還需要走其餘的橋面(申請新的資源),但那部分橋面被乙佔有(乙走過一段橋面)。甲過不去,前進不能,又不後退;乙也處於同樣的狀況。
〈4〉迴圈等待條件。存在一個程序等待序列{P1,P2,...,Pn},其中P1等待P2所佔有的某一資源,P2等待P3所佔有的某一源,......,而Pn等待P1所佔有的的某一資源,形成一個程序迴圈等待環。就像前面的過獨木橋問題,甲等待乙佔有的橋面,而乙又等待甲佔有的橋面,從而彼此迴圈等待。
死鎖預防:
前面介紹了死鎖發生時的四個必要條件,只要破壞這四個必要條件中的任意一個條件,死鎖就不會發生。這就為我們解決死鎖問題提供了可能。一般地,解決死鎖的方法分為死鎖的預防,避免,檢測與恢復三種(注意:死鎖的檢測與恢復是一個方法)。我們將在下面分別加以介紹。
死鎖的預防是保證系統不進入死鎖狀態的一種策略。它的基本思想是要求程序申請資源時遵循某種協議,從而打破產生死鎖的四個必要條件中的一個或幾個,保證系統不會進入死鎖狀態。
1打破互斥條件。即允許程序同時訪問某些資源。但是,有的資源是不允許被同時訪問的,像印表機等等,這是由資源本身的屬性所決定的。所以,這種辦法並無實用價值。
2打破不可搶佔條件。即允許程序強行從佔有者那裡奪取某些資源。就是說,當一個程序已佔有了某些資源,它又申請新的資源,但不能立即被滿足時,它必須釋放所佔有的全部資源,以後再重新申請。它所釋放的資源可以分配給其它程序。這就相當於該程序佔有的資源被隱蔽地強佔了。這種預防死鎖的方法實現起來困難,會降低系統性能。
3打破佔有且申請條件。可以實行資源預先分配策略。即程序在執行前一次性地向系統申請它所需要的全部資源。如果某個程序所需的全部資源得不到滿足,則不分配任何資源,此程序暫不執行。只有當系統能夠滿足當前程序的全部資源需求時,才一次性地將所申請的資源全部分配給該程序。由於執行的程序已佔有了它所需的全部資源,所以不會發生佔有資源又申請資源的現象,因此不會發生死鎖。但是,這種策略也有如下缺點:
(1)在許多情況下,一個程序在執行之前不可能知道它所需要的全部資源。這是由於程序在執行時是動態的,不可預測的;
(2)資源利用率低。無論所分資源何時用到,一個程序只有在佔有所需的全部資源後才能執行。即使有些資源最後才被該程序用到一次,但該程序在生存期間卻一直佔有它們,造成長期佔著不用的狀況。這顯然是一種極大的資源浪費;
(3)降低了程序的併發性。因為資源有限,又加上存在浪費,能分配到所需全部資源的程序個數就必然少了。
4打破迴圈等待條件,實行資源有序分配策略。採用這種策略,即把資源事先分類編號,按號分配,使程序在申請,佔用資源時不會形成環路。所有程序對資源的請求必須嚴格按資源序號遞增的順序提出。程序佔用了小號資源,才能申請大號資源,就不會產生環路,從而預防了死鎖。這種策略與前面的策略相比,資源的利用率和系統吞吐量都有很大提高,但是也存在以下缺點:
(1)限制了程序對資源的請求,同時給系統中所有資源合理編號也是件困難事,並增加了系統開銷;
(2)為了遵循按編號申請的次序,暫不使用的資源也需要提前申請,從而增加了程序對資源的佔用時間。
死鎖的避免:
上面我們講到的死鎖預防是排除死鎖的靜態策略,它使產生死鎖的四個必要條件不能同時具備,從而對程序申請資源的活動加以限制,以保證死鎖不會發生。下面我們介紹排除死鎖的動態策略--死鎖的避免,它不限制程序有關申請資源的命令,而是對程序所發出的每一個申請資源命令加以動態地檢查,並根據檢查結果決定是否進行資源分配。就是說,在資源分配過程中若預測有發生死鎖的可能性,則加以避免。這種方法的關鍵是確定資源分配的安全性。
死鎖的例子:
public class DeadLock implements Runnable {
public int flag = 1;
// 靜態物件是類的所有物件共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
// td1,td2都處於可執行狀態,但JVM執行緒排程先執行哪個執行緒是不確定的。
// td2的run()可能在td1的run()之前執行
new Thread(td1).start();
new Thread(td2).start();
}
}
一個簡單的死鎖類 當DeadLock類的物件flag==1時(td1),先鎖定o1,睡眠500毫秒
* 而td1在睡眠的時候另一個flag==0的物件(td2)執行緒啟動,先鎖定o2,睡眠500毫秒
* td1睡眠結束後需要鎖定o2才能繼續執行,而此時o2已被td2鎖定; td2睡眠結束後需要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
* td1、td2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。
執行緒通訊:
傳統的執行緒通訊:
首先介紹三個方法:
Object類下面的wait(),notify(),notifyAll()
這三個方法必須由同步監視器物件來呼叫,可以分為兩種情況:
synchronized修飾的同步方法,該類的預設例項(this)就是同步監視器,所以可以在同步方法直接呼叫這三個方法
synchronized修飾的同步程式碼塊,同步監視器synchronized後括號裡的物件,所以必須使用該物件呼叫這三個方法
關於這三個方法的解釋:
wait():導致當前執行緒等待,直到其它執行緒呼叫該同步監視器的notify或notifyAll()來喚醒該執行緒
notify():喚醒在此同步監視器等待的單個執行緒,如果多個執行緒在此同步監視器等待,則會任意選擇一個喚醒
notify()喚醒在此同步監視器等待的所有執行緒。
銀行取錢案例:
賬戶類:取錢方法,存錢方法
public class Account {
private String accountNo;
private double balance;
private boolean flag=false;//已有存款標誌
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public synchronized void draw(double drawAmount){
try{
if(!flag){//為假,所以沒有人存錢進去,取錢阻塞
wait();
}else{//可以取錢
if(balance>=drawAmount){
System.out.println(Thread.currentThread().getName()+"取錢:"+drawAmount);
balance-=drawAmount;
System.out.println("取錢成功,賬戶餘額:"+balance);
flag=false;
notifyAll();//喚醒存錢執行緒
}else{//餘額不足
System.out.println("想取"+drawAmount+"賬戶餘額不足:"+balance);
flag=false;
notifyAll();//喚醒存錢執行緒
}
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount){
try{
if(flag){
wait();//沒人取錢,則存錢阻塞
}else{
System.out.println(Thread.currentThread().getName()+"存錢:"+depositAmount);
balance+=depositAmount;
System.out.println("存錢成功,賬戶餘額:"+balance);
flag=true;
notifyAll();//喚醒取錢執行緒
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}
}
取錢執行緒(消費者)
public class DrawThread extends Thread{
private Account account;
private double drawAmount;//取錢數
public DrawThread(String name,Account account,double drawAmount){
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
public void run() {
for(int i=0;i<10;i++){
account.draw(drawAmount);
}
}
}
存錢執行緒(生產者)
public class DepositThread extends Thread{
private Account account;
private double depositAmount;//取錢數
public DepositThread(String name,Account account,double depositAmount){
super(name);
this.account=account;
this.depositAmount=depositAmount;
}
public void run() {
for(int i=0;i<10;i++){
account.deposit(depositAmount);
}
}
}
測試類:
public class AccountTest {
public static void main(String[] args) {
Account account=new Account("123456", 0);
new DrawThread("取款者lry", account, 1200).start();
new DepositThread("存款者甲", account, 1000).start();
new DepositThread("存款者乙", account, 1000).start();
new DepositThread("存款者丙", account, 1000).start();
}
}
測試結果:
測試結果分析:
不難發現取錢執行緒和存錢執行緒交替執行,只有當取錢者取錢後,存款者才可以存款,同理,只有當存款者存款後,取錢者才可以取錢,程式最終顯示被阻塞無法繼續執行,這是因為有三個存款執行緒,但是取錢只有一個執行緒。這並不是死鎖,這種情況只是取錢執行緒已經執行完畢,而存款執行緒並沒有,她在等待其它執行緒來取錢而已,並不是等待其它執行緒釋放同步監視器。
使用Lock+Condition控制執行緒通訊:
如果程式中不使用synchronized來保證同步,而是直接使用Lock來保證同步,則系統中不存在隱式的同步監視器,也就不能用wait,notify,notifyAll來進行執行緒通訊了。
當使用lock時,java提供了一個Conditin類保持協調,使用lock物件可以讓那些已經得到lock物件卻無法繼續執行的執行緒釋放lock物件,conditin物件也可以喚醒其他處於等待的執行緒。condition提供了三個方法用法分別對應Object類的wait,notify,notifyAll,分別是await,signal,signalAll。用法相似,不再贅述。
還是引用上個取款案例:
只是修改了Account賬戶類:
public class Account1 {
private String accountNo;
private double balance;
private boolean flag=false;//已有存款標誌
private final Lock lock=new ReentrantLock();
private final Condition cond=lock.newCondition();
public Account1(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public void draw(double drawAmount){
lock.lock();
try{
if(!flag){//為假,所以沒有人存錢進去,取錢阻塞
cond.await();
}else{//可以取錢
if(balance>=drawAmount){
System.out.println(Thread.currentThread().getName()+"取錢:"+drawAmount);
balance-=drawAmount;
System.out.println("取錢成功,賬戶餘額:"+balance);
flag=false;
cond.signalAll();//喚醒存錢執行緒
}else{//餘額不足
System.out.println("想取"+drawAmount+"賬戶餘額不足:"+balance);
flag=false;
cond.signalAll();//喚醒存錢執行緒
}
}
}catch(InterruptedException ex){
ex.printStackTrace();
}finally {
lock.unlock();
}
}
public void deposit(double depositAmount){
lock.lock();
try{
if(flag){
cond.await();//沒人取錢,則存錢阻塞
}else{
System.out.println(Thread.currentThread().getName()+"存錢:"+depositAmount);
balance+=depositAmount;
System.out.println("存錢成功,賬戶餘額:"+balance);
flag=true;
cond.signalAll();//喚醒取錢執行緒
}
}catch(InterruptedException ex){
ex.printStackTrace();
}finally {
lock.unlock();
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account1 other = (Account1) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}
}
測試結果是一樣的。
使用阻塞佇列(BlockingQueue)控制執行緒通訊:
java5提供了一個BlockingQueue介面,雖然這個介面是queue的子介面,但它的主要用途不是作為容器,而是作為執行緒同步的工具。BolckingQueue具有一個特徵,當生產者試圖向BolckingQueue裡put元素,如果佇列已滿,則該執行緒會被阻塞,直到消費者消費了一個。當消費者試圖從blockingQueue裡take元素時,如果佇列為空,則會阻塞,直到生產者生產了一個。
在佇列尾部插入元素,包括add,offer,put,當佇列已滿時,這三個方法分別丟擲異常,返回false,阻塞
在佇列頭部刪除並返回刪除的元素,包括remove,poll,take,當佇列已空時,這三個方法分別會丟擲異常,返回false,阻塞
在佇列頭部取出元素,不刪除。包括element,peek,當佇列已空時,分別丟擲異常,返回false
經典生產者-消費者案例:
生產者:
public class Producer extends Thread{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq){
this.bq=bq;
}
@Override
public void run() {
String[] strArr=new String[]{"java","structs","spring"};
for(int i=0;i<1000;i++){
System.out.println(getName()+"生產者準備生產集合元素");
try {
Thread.sleep(200);
//嘗試put元素,如果佇列已滿,則阻塞
bq.put(strArr[i%3]);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"生產者生產完成"+bq);
}
}
}
消費者:
public class Consumer extends Thread{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq){
this.bq=bq;
}
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println(getName()+"消費者準備消費集合元素");
try {
Thread.sleep(200);
//嘗試put元素,如果佇列已滿,則阻塞
bq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"消費者消費完成"+bq);
}
}
}
測試類:
public class BlockingQueueTest {
public static void main(String[] args) {
BlockingQueue<String>bq=new ArrayBlockingQueue<String>();
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
new Consumer(bq).start();
}
}
測試結果:
測試結果分析:
可以看出,3個生產者執行緒都想向佇列中put元素,但只要其中任意執行緒put元素後,其它生產者必須等待(因為阻塞),等待消費者消費完。
執行緒組和未處理的異常:
執行緒組:
java使用ThreadGroup來表示執行緒組,他可以對一批執行緒進行分類管理,java允許程式直接對執行緒組進行控制,對執行緒組的控制相當於同時控制這批執行緒。
如果程式沒有顯示指定執行緒屬於哪個執行緒組,則該執行緒屬於預設執行緒組。在預設情況下,子執行緒和建立它的父執行緒處於同一個執行緒組內。
一旦但執行緒加入了指定執行緒組後,該執行緒一直屬於該執行緒組,直到死亡,執行緒執行中途不能改變他所屬的執行緒組。
Thread類提供瞭如下幾個構造器來設定新建立的執行緒屬於哪個執行緒組。
Thread(ThreadGroup group,Runnable tartget):
Thread(ThreadGroup group,Rannable target,String name);
Thread(ThreadGrop group,String name)
Thread沒有提供setThreadGroup(),但是提供getThreadGroup返回ThreadGroup物件。
ThreadGroup提供瞭如下兩個簡單的構造器來建立例項。
ThreadGroup(String name)以指定的執行緒組名字來建立新的執行緒組。
ThreadGroup(ThreadGroup parent,String name)以指定的名字,指定的父執行緒組建立一個執行緒組
ThreadGrop還提供了幾個常用的方法來操作整個執行緒組的所有執行緒。
int activeCount()返回此執行緒組中活動執行緒的數目
interrupt()中斷此執行緒組的所有執行緒
isDaemon()判斷該執行緒組是否是後臺執行緒組
setDaemon(boolean daemon)把該執行緒組設為後臺執行緒組
setMaxPriority(int pri)設定執行緒組的最高優先順序
案例:
public class TestThreadGroup extends Thread{
public TestThreadGroup(String name){
super(name);
}
public TestThreadGroup(ThreadGroup group,String name){
super(group,name);
}
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println(getName()+"-執行緒的i變數"+i);
}
}
public static void main(String[] args) {
ThreadGroup mainGroup=Thread.currentThread().getThreadGroup();
System.out.println("主執行緒組的名字:"+mainGroup.getName());
new TestThreadGroup("主執行緒組的執行緒").start();
ThreadGroup tg=new ThreadGroup("新執行緒組");
tg.setDaemon(true);
System.out.println("新執行緒組是否是後臺執行緒組"+tg.isDaemon());
new TestThreadGroup(tg,"新執行緒組").start();
}
}
結果:
未處理的異常:
Thread裡還定義一個很有用的方法:void uncaughtExecptin(Thread t,Throwable e),該方法可以處理該執行緒組內任意執行緒所丟擲未處理異常。
從java5開始,java加強了執行緒的異常處理,如果執行緒執行過程中丟擲了一個未處理異常,jvm在結束該執行緒之前會自動查詢是否有對應的Thread.UncaughtExecptionHandler物件,如果找到該處理器的物件,則會呼叫該物件的uncaughtExecption(Thread t,Throwable e)方法來處理該異常。
如果該執行緒組有父執行緒組,則呼叫父執行緒組的uncaughtException來處理異常
如果該執行緒例項所屬的執行緒類有預設的異常處理器(由setDefaultUncaughtExecptionHandler()設定),那麼就呼叫該異常處理器來處理該異常
如果該異常物件是ThreadDeath的物件,則不做任何處理,否則將異常跟蹤棧的資訊列印到system.err錯誤輸出流,並結束該執行緒。
案例:
public class TestThreadExHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t+"執行緒出現異常:"+e);
}
public static void main(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler(new TestThreadExHandler());
int i=1/0;
System.out.println("程式正常結束");
}
}
結果:
分析:
不難看出異常處理器捕獲到異常後,程式仍然不會正常退出。
這說明異常處理器與通過catch捕獲異常是不同的,當使用catch捕獲異常時,異常不會向上傳遞給上一級呼叫者,但是異常處理器會。
執行緒池:
系統啟動一個新執行緒的成本是比較高的,因為他涉及與作業系統互動,在這種情況下,就誕生了執行緒池。
Java8改進的執行緒池:
java5之前開發者需要自己實現自己的執行緒池,java5後,新增了一個Executors工廠類來產生執行緒池,該工廠類包含如下幾個靜態方法來建立執行緒池。
newCacheThreadPool()建立一個具有快取功能的執行緒池,系統根據需要建立執行緒,這些執行緒將會被快取線上程池中。
newFixedThreadPool(int nThreads)建立一個可重用的,具有固定執行緒數的執行緒池
newSingleThreadExecutor()建立一個只有單執行緒的執行緒池,它相當於呼叫newFixedThreadPool(1)
newScheduledThreadPool(int corePoolSize)建立具有指定執行緒數的執行緒池
newSingleThreadScheduledExecutor()建立一個只有一個執行緒的執行緒池
ExecutorService newWorkStealingPool(int parallelism)建立持有足夠的執行緒的執行緒池來支援給定的並行級別,該方法會使用多個佇列來減少競爭。(後臺執行緒池)
ExecutorService newWorkStealingPool()該方法是前一個方法的簡化版,如果當前機器有4個cpu,則目標並行級別被設定為4。(後臺執行緒池)
案例1實現自己的執行緒池:
public class ThreadPool {
LinkedBlockingQueue<Runnable>workQueue=new LinkedBlockingQueue<>(100);
public static void main(String[] args) {
new ThreadPool().threadPool();
}
public void threadPool(){
ThreadFactory threadFactory=new ThreadFactory() {
AtomicInteger atomic=new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread=new Thread(r);
thread.setName("MyThread"+atomic.getAndIncrement());
return thread;
}
};
/**corePoolSize核心池最大數量
* maximumPoolSize最大執行緒池上限個數
* keepAliveTime任務執行完,銷燬執行緒的延時
* unit時間單位 TimeUnit.SECONDS;
* workQueue 用於儲存任務的工作佇列
* threadFactory
*/
ThreadPoolExecutor pool=new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, workQueue, threadFactory);
for(int i=0;i<100;i++){
pool.execute(new Runnable() {
public void run() {
method();
}
});
}
}
private void method(){
System.out.println("ThreadName:"+Thread.currentThread().getName()+"進來了");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadName:"+Thread.currentThread().getName()+"出去了");
}
}
結果:
案例二用java自帶的執行緒池:
public class TestJavaThreadPool {
public static void main(String[] args) {
ExecutorService pool=Executors.newFixedThreadPool(6);
Runnable target=new Runnable() {
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThrea