第一節 JAVA線程停止的錯誤方法
stop方法,no stop 這不是正確的方法,會讓我們的程序戛然而止,會使我們不知道哪些工作沒做,完成了什么任務以及沒有機會去做清理工作。使用的結果會造成程序突然停止,強行關閉,有時一個循環可能都沒做完。
JAVA停止線程的正確做法—設置退出旗標,使用退出標志來停止線程,如之前的程序先設置一個布爾類型的值,volatile類型來保證每次都能讀取到它的值,賦值false來退出線程。
JAVA停止線程廣為流傳的錯誤方法—interrupt方法,interrupt初衷並不是停止我們的線程。
查詢JAVA API文檔,在java.lang包下,找到Thread,Ctrl+F找到interrupt(),找到三個。
interrupt() 中斷線程
interrupted() 測試當前線程是否已經中斷,注意這個方法是靜態方法。
isInterrupted() 測試線程是否已經中斷。后者兩個方法返回的值都是布爾值。
在API中我們看到:如果線程在調用Object類的wait()、wait(long)或wait(long, int)方法,或者該類的join()、join(long)、join(long, int)、sleep(long)或 sleep(long, int)方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException。
在這里首先我們看到API中interrupt()方法中斷線程是有條件的,在API中提示如果以前的條件都沒有保存,才會將該線程的中斷狀態設置。此時調用后面的interrupted()或者isInterrupted()將返回一個布爾值的變量true來表示線程被中斷。
如果使用了join方法或者sleep方法使得線程阻塞中斷的情況下,使用interrupet會使得線程的中斷狀態被清除,並且當前的線程將會收到一個InterruptedException,這代表如后面再調用interrupted或者isInterrupted方法將不會得到一個正確的值。這就是為什么我們在使用join方法或者sleep方法需要用try-catch語句包圍來捕獲這個InterruptedException異常的原因。在使用join或者sleep方法時,一旦其它或當前線程調用了interrupted()方法,它將會收到一個異常。這些被阻塞的線程因為某些原因需要被喚醒,比如外部發生了中斷,它需要響應,這時它就通過拋出異常的方式來使我們有機會做出一些響應。所以interrupt並不能正確的停止我們的線程。
下面使用代碼來實際演示一下:
/**
* Created by Administrator on 2017/4/7.
*/
public class WrongWayStopThread extends Thread {
public void run(){
while(!this.isInterrupted()){
//!this.isInterrupted() 一開始使用的是true,線程停止不了,更換后
//正確退出線程,此方法和設置退出標志法一樣,只是退出旗標比較特殊
//線程是否被中斷的一個狀態
System.out.println("Thread is running...");
//獲取當前系統時間的一個毫秒值
long time=System.currentTimeMillis();
//long time1=System.nanoTime();這個精度到納秒,
//比上面currentTimeMillis()毫秒精度高
//while((System.currentTimeMillis()-time)<1000){
//減少我們的屏幕輸出,大概等於sleep休眠1000毫秒
//也就是每秒中輸出一條信息("Thread is running..."
// 這里為什么沒有使用sleep呢?
//}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用sleep方法運行結果,拋出異常,程序沒有正確結束,
// sleep方法使線程進入一種阻塞狀態之時,此時如果這個//
// 線程再被調用interrupt方法,它會產生兩個結果,
// 1是它的中斷狀態被清除,線程的isInterrupted就不能
// 返回一個表示是否被中斷的正確狀態,那么這里的while
// 就不能正確的退出了。
//2第二個結果就是收到一個InterruptedException異常表明
// 它被中斷了
}
}
public static void main(String[] args) {
WrongWayStopThread thread=new WrongWayStopThread();
//新建一個線程 thread。這里父類子類引用效果應該是一樣的吧?先照
//示例演示一遍之后再更換成 Thread類型嘗試下
System.out.println("Starting thread...");
//啟動線程
thread.start();
//休眠3秒鍾並且用try-catch語句塊包圍
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Interrupting thread...");
//用 interrupt方法中斷線程,注意前面調用了sleep方法阻塞中斷線程3秒鍾
thread.interrupt();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Stopping application");
}
}
第二節 JAVA線程交互之汽車人之憂:消失的能量
程序的任務是建立一個能量盒子的數組,初始值里數組里每個盒子是一個固定的初始值總能量也固定,然后互相傳遞能量。在線程實現后有部分情況下能量出現丟失。
先建立一個宇宙的能量系統
1.定義了一個私有的數組常量,並且在初始化構造方法中,先用一個參數n表示數組的長度,再利用for循環把數組每個值都設置為統一固定的參數值。
2.定義了一個能量傳遞的方法,參數為(傳遞初的位置下標from,傳遞后的終點位置的下標to,傳遞的能量值),方法體第一句表示如果最初的這個盒子能量不滿足本次傳遞則終止本次轉出,if(條件),return;然后是方法體內的公式,數組[from]-=傳遞的能量值,能量轉出后對方要數組[to]+=傳遞的能量值。中間插入輸出語句,用printf格式占位輸出。注意只是傳遞了一次。
System.out.printf("從%d轉移到%10.2f單位能量到%d",from,amount,to);%d對應我們參數中的from,%d代表輸出一個整數值,%10.2f代表我們輸出一個浮點數,並且它的小數部分是兩位,整數點之前的部分應該有十位。System.out.printf("能量總和:%10.2f%n",getTotalEnergies());我們這里的%n則類似我們的轉義字符反斜杠n /n 代表了一個回車換行符。
3.定義一個方法這里我們需要獲取這個能量世界的總和。利用for循環遍歷其中的數組元素並設立一個變量初始值為0,累計相加最后得出並輸出一個總和
4.設置一個方法直接返回當前數組的長度.length
/**
* Created by Administrator on 2017/4/7.
* 宇宙的能量系統
* 遵循能量守恆的定律
* 能量不會憑空消失,只會從一處轉移到另一處
*/
public class EnergySystem {
//能量盒子的總和,貯存能量的地方,每個盒子存儲了一定的能量
//所有的盒子能量之和代表宇宙的總能量
private final double[] energyBoxes;
/**
*
* @param n 代表了能量盒子的數量
* @param initialEnergy 每個能量盒子初始含有的能量值
*/
public EnergySystem(int n,double initialEnergy) {
this.energyBoxes = new double[n];
for(int i=0;i<energyBoxes.length;i++){
energyBoxes[i]=initialEnergy;
}
}
//這個構造方法會使這個double數組一開始所有的元素值相等,
// 總和=n*initialEnergy,數組構造還是第一次見
/**
*能量的轉移,從一個盒子到另一個盒子
* @param from 能量源
* @param to 能量終點
* @param amount 轉移的能量值
*/
public void transfer(int from,int to,double amount){
//能量轉出的值不足以滿足本次能量轉出時,我們則會終止本次轉出
if(energyBoxes[from]<amount)
return;
//這個return指的是符合條件則跳出當前方法
System.out.print(Thread.currentThread().getName());
energyBoxes[from]-=amount;
System.out.printf("從%d轉移到%10.2f單位能量到%d",from,amount,to);
//printf格式輸出,它利用特別的字符來占位,然后在我們后面的輸出列表
// 之中,對應其真實的值,例如%d對應我們參數中的from,%d代表輸出一個
// 整數值,%10.2f代表我們輸出一個浮點數,並且它的小數部分是兩位,
// 整數點之前的部分應該有十位。
energyBoxes[to]+=amount;
System.out.printf("能量總和:%10.2f%n",getTotalEnergies());
//我們這里的%n則類似我們的轉義字符反斜杠n /n 代表了一個回車換行符
}
/**
* 獲取能量世界的能量總和
*/
public double getTotalEnergies(){
double sum=0;
for (double amount:energyBoxes){
sum+=amount;
}
return sum;
}
/**
* 返回能量合子的長度
*/
public int getBoxAmount(){
return energyBoxes.length;
}
}
上一個類只是一個宇宙能量系統的類, 提供了能量傳遞的方法和計算總能量和返回數組長度的方法,定義了能量盒子數組。第二個類是我們的線程任務類,能量傳遞任務,一共設置了四個屬性,第一個是EnergySsytem,因為只使用一個能量世界,定義了一個EnergySystem能量系統的屬性,並且把對象當做構造方法參數傳遞進去。
定義了一個能量轉移的盒子下標 fromBox,這個應該不能超過數組的長度,因為下標從0開始,0-長度-1,還有每次可以轉移的最大能量值,設置了一個休眠時間,輸入一個帶參構造方法,復寫run方法,能量傳遞的值和toBox,目的盒子的下標,兩者都是隨機數。但不能超過最大值,故選擇最大值Math.random()方法,等於或大於0.0,小於1.0之間的隨機數,調取傳遞方法后休眠Math.random()隨機時常,whiletrue循環,注意try塊把try放在循環體外
package energysystem;
/**
* Created by Administrator on 2017/4/7.
*/
public class EnergyTransferTask implements Runnable{
//共享的能量世界
private EnergySystem energySystem;
//能量轉移的源能量盒子下標
private int fromBox;
//單詞能量轉移最大單元
private double maxAmount;
//最大休眠時間毫秒
private int DELAY=10;
public EnergyTransferTask(EnergySystem energySystem,int from,double max){
this.energySystem=energySystem;
this.fromBox=from;
this.maxAmount=max;
}
@Override
public void run() {
try {
while(true){
int toBox=(int)(energySystem.getBoxAmount()*Math.random());
//Math.random()是令系統隨機選取大於等於 0.0
且小於 1.0 的偽隨機 double 值
//長度假如說是10*0.9=9,因為不會等於1,所以無論如何也不會超過限度。
double amount=maxAmount*Math.random();
energySystem.transfer(fromBox,toBox,amount);
Thread.sleep((int)(DELAY*Math.random()));
}
}catch (InterruptedException e) {
//注意把try塊try的開頭移到循環體之外,在內部的話如果有異常會在循環后來回
// 拋出,容易造成內存溢出,在外面美觀好用
e.printStackTrace();
}
}
}
運行類
package energysystem;
/**
* Created by Administrator on 2017/4/7.
*/
public class EnergySystemTest {
//將要構建的能量世界中能量盒子數量
public static final int BOX_AMOUNT=100;
//每個盒子的初始能量
public static final double INITIAL_ENERGY=1000;
public static void main(String[] args) {
EnergySystem eng=new EnergySystem(BOX_AMOUNT,INITIAL_ENERGY);
for(int i=0;i<BOX_AMOUNT;i++){
EnergyTransferTask task=new
EnergyTransferTask(eng,i,INITIAL_ENERGY);
Thread t=new Thread(task,"TransferThread_"+i);
t.start();
}
}
}
第三節 JAVA線程之爭用條件
上一節程序最后的結果是在不斷的傳遞能量過程中,總能量出現減少,而出現這種情況就是由於我們的爭用條件問題造成的。
什么是爭用條件呢?
利用一個生活化的例子,女神往往受到很多追求者青睞,而女神最后只能選擇一個,當她同時答應多個人的追求時,將不免發生流血事件。同理,把女神比作數據(內存區域)
當多個線程同時共享訪問同一數據(內存區域)時,每個線程都嘗試操作該數據,從而導致數據被破壞,這種現象我們就稱之為爭用條件。
以上面的代碼為例,線程1和線程2共享了同一個能量轉移目標,同時我們知道,在同一時間只能有一個線程在cpu上運行,而線程之間的調度則是通過分時和搶度完成的。線程一轉入500,線程二轉入900,由於爭用條件最后賦值還是線程1轉入的5500
第四節 能量守恆:互斥和同步
互斥:相互排斥,在同一時間里只能有一條線程去對我們的關鍵數據或者臨界區進行操作,同步:歸根結底,就是線程之間的一種通信機制。一條線程執行完了某項任務,它會以某種方式告知其它線程,我做完了。在這里我們要提到關鍵字 synchronized ,還有對象的.wait 和notifyAll()。新增添一個常量類型是Object對象,作為我們的線程鎖
private final Object lockObj=new Object();
//定義了一個常量,這個是我們的鎖對象
再去程序中修改一下能量轉移的方法
public void transfer(int from,int to,double amount){
//能量轉出的值不足以滿足本次能量轉出時,我們則會終止本次轉出
//synchronized既可以出現於我們的方法體之上也可以出現在我們的方法體之中
//把這個不滿足能量退出的方法加入synchronized修改一下。
//if(energyBoxes[from]<amount)
// return;
//這個return指的是符合條件則跳出當前方法
synchronized(lockObj){
//這里的this一開始填的lockObj,通過對對象的加鎖來實現我們的互斥行為,
//已知加鎖必須是一個共享且唯一的,后面我們把該對象作為參數傳遞
//入線程,此時是共享一個對象了
//if(energyBoxes[from]<amount)
//return;
//上面這個方法表示不滿足轉出則程序退出,現在想想退出之后我們的這段
// 線程能然有機會去獲取CPU資源,從而再次要求進行加鎖,而我們的加鎖
// 操作是有開銷的,(之前的話不需要加鎖,大不了多試幾次退出幾次)
// 這樣會降低我們系統的性能。那么好的辦法是什么呢?
// 當我們發現條件不滿足時,這時我們應該讓線程去等待某些條件的發生
// 從而降低這個線程去獲取鎖的開銷,提高我們整體的性能。
//while循環,保證條件不滿足時任務都會被條件阻擋,而不是去競爭我們
// 的CPU資源
while(energyBoxes[from]<amount){
//當我們發現線程不滿足某些條件時,應該將線程阻擋在我們業務邏輯
// 之前
try {
lockObj.wait();
//wait Set等待集合
} catch (InterruptedException e) {
e.printStackTrace();
}
//這會使我們的線程進入等待狀態,而避免了我們的線程去持續的申請鎖
}
System.out.print(Thread.currentThread().getName());
energyBoxes[from]-=amount;
System.out.printf("從%d轉移到%10.2f單位能量到%d",from,amount,to);
//printf格式輸出,它利用特別的字符來占位,然后在我們后面的輸出列表
// 之中,對應其真實的值,例如%d對應我們參數中的from,%d代表輸出一個
// 整數值,%10.2f代表我們輸出一個浮點數,並且它的小數部分是兩位,
// 整數點之前的部分應該有十位。
energyBoxes[to]+=amount;
System.out.printf("能量總和:%10.2f%n",getTotalEnergies());
lockObj.notifyAll();
//我們這里的%n則類似我們的轉義字符反斜杠n /n 代表了一個回車換行符
//這里我們應該通知被阻擋的線程,告訴被等待的線程,條件發生了變化,他們
//有可能能被執行了
//喚醒所有lockObj對象上等待的線程。
}
}
此時再運行我們的程序,能量就守恆了。
第五節 深度剖析
什么是互斥,互斥是怎么實現的?互斥也就是說關鍵數據在同一時間只能被一個線程所訪問。互斥的實現 synchronized(intrinsic lock) 單詞含義固有的鎖,synchronized(this)相當於給我們的代碼加一把鎖,使得其他線程不能進我們這個關鍵區域去訪問關鍵資源。只有獲得了(this)鎖的線程能夠進入這個核心區域。java的語法保證了同一時間,只能有一條線程獲得我們的線程鎖。上面的例子就是新建了一個Object對象作為鎖,感覺換成this也完全可以實行,因為共享了一個能量世界,鎖住的方法也是這個類的方法。
同步的實現 wait() notify() notifyAll();這三個方法屬於object對象而不是thread對象,object對象的成員函數。如之前的代碼:
lockObj.wait();
lockObj.notifyAll();
當能量不滿足不能轉移時,調用對象的wait方法讓該線程進入wait set等待的集合,然后繼續下一個線程,當其他線程執行完畢喚醒所有等待線程。(當有線程執行完,使之前的線程能量得到滿足,擁有足夠的能量可以滿足本次轉移時,繼續執行。)注意這里的wait和notifyAll不是同一個線程哦。同步是兩個線程之間的交互。wait set,是線程休息室。critical section 我們的共享資源共享數據又被稱為臨界區,當有一個線程訪問我們的資源時,首先它需要獲得一把鎖,當它獲得了鎖它將進入我們的臨界區操作,操作過程中它發現某些情況不被滿足,比如能量不滿足轉移。它將調用我們鎖對象上的wait方法,此時這個線程首先釋放掉我們的鎖資源,然后進入我們的鎖對象上的wait set。由於這個線程釋放掉了我們的鎖資源,可以讓其他線程來競爭我們的鎖資源。所以我們看到其他線程獲得鎖並進入了我們的臨界區,同時我們看到waitset中有多條線程在等待條件的滿足。當我們當前線程執行完某些操作,需要通知等待的線程時,調用我們的notify()方法 將會喚醒我們鎖資源所持有的等待區域中的1條線程,使這條線程有機會去競爭cpu資源。notifyAll()將喚醒所有等待的線程,去競爭臨界區的鎖對象。
第六節 總結及展望
在這里我們學習了:
1.如何創建線程及線程的基本操作
2.可見性及volatile關鍵字實現線程的可見性編程
3.爭用條件
4.線程的互斥synchronized
5.線程的同步wait/notifyAll
擴展建議
如何擴展java並發的知識:
1.Java Memory Mode
JMM描述了java線程如何通過內存進行交互,通過這部分知識我們可以了解什么是happens-before原則,為什么要使用happens-before原則,java是如何通過synchronized,volatile&final關鍵字來實現這一原則的。
2.另外可以看看Lockc&Condition這兩個對象
這兩個是java5.0后引入的是對我們java鎖機制和等待條件的高層實現,通過它我們可以了解我們如何對程序實現加鎖以及同步的通信,結合我們的synchronized和wait以及notifyAll方法,大家可以更好的理解我們的互斥與同步是如何實現的。 java.util.concurrent.locks
3.另一部分我們可以了解下線程的安全性
什么是原子性與可見性的問題,如何通過atomic包來避免我們原子性編程的問題,java.util.concurrent.atomic,同時當我們的一個原子操作由多個操作語句構成時,我們又是如何通過synchronized的方法來實現我們的原子型操作,類似我們如何通過synchronized&volatile 來實現我們的可見性編程,最后大家需要了解什么是死鎖,死鎖產生的條件是什么,進而可以書寫出一些避免死鎖發生的線程DeadLocks
4.另外一方面多線程編程常用的交互模型
Producer-Consumer模型 經典的生產者消費者模型
Read-write Lock模型 讀寫鎖模型
Future模型
Worker Thread模型
在了解這些模型的基礎之上,大家可以考慮在我們的java並發實現當中,有哪些類是實現了這些模型可以供我們直接調用的。
5.最后一方面就是java5引入的一些並發編程工具
java.util.concurrent,都在這個包之下
線程池 ExecutorService
Callable&Future對象
BlockingQueue對象
大大簡化我們我們之前的線程編程模型,使我們可以更加方便的使用一種面向於任務,更加接近於實際應用需求的一種抽象方式來書寫我們的線程,使我們線程有了更大的利用空間。
最后推薦兩本書 core-java 第九版。
java編程的聖經 java concurrency in practice