張雲飛 201771010143 《面對對象程序設計(java)》第十七周學習總結
1、實驗目的與要求
(1) 掌握線程同步的概念及實現技術;
(2) 線程綜合編程練習
2、實驗內容和步驟
實驗1:測試程序並進行代碼註釋。
測試程序1:
l 在Elipse環境下調試教材651頁程序14-7,結合程序運行結果理解程序;
l 掌握利用鎖對象和條件對象實現的多線程同步技術。
package synch;
import java.util.*;
import java.util.concurrent.locks.*;
/**
* 有許多銀行賬戶的銀行,它使用鎖來序列化訪問
* @version 1.30 2004-08-01
* @author Cay Horstmann
*/
public class Bank
{
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
/**
* 建造銀行
* @param n 帳戶數量
* @param initialBalance 每個帳戶的初始余額
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);//將initialBalance分配給accounts數組的每個元素。
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();//返回綁定到此 Lock 實例的新 Condition 實例
}
/**
* 將資金從一個帳戶轉移到另一個帳戶
* @param from 帳戶轉帳來自
* @param to 帳戶轉帳到
* @param amount 轉賬金額
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();//將該線程放在條件的等待集中
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();//喚醒所有等待線程
}
finally
{
bankLock.unlock();
}
}
/**
* 獲取所有帳戶余額的總和
* @return 總余額
*/
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
/**
* 獲取銀行中的帳戶數量
* @return 帳戶數量
*/
public int size()
{
return accounts.length;
}
}
Bank
實驗結果:
測試程序2:
l 在Elipse環境下調試教材655頁程序14-8,結合程序運行結果理解程序;
l 掌握synchronized在多線程同步中的應用。
實驗結果:
測試程序3:
l 在Elipse環境下運行以下程序,結合程序運行結果分析程序存在問題;
l 嘗試解決程序中存在問題。
class Cbank { private static int s=2000; public static void sub(int m) { int temp=s; temp=temp-m; try { Thread.sleep((int)(1000*Math.random())); } catch (InterruptedException e) { } s=temp; System.out.println("s="+s); } }
class Customer extends Thread { public void run() { for( int i=1; i<=4; i++) Cbank.sub(100); } } public class Thread3 { public static void main(String args[]) { Customer customer1 = new Customer(); Customer customer2 = new Customer(); customer1.start(); customer2.start(); } } |
修改後的代碼:
package a;
class Cbank
{
private static int s=2000;
public synchronized static void sub(int m)
{
int temp=s;
temp=temp-m;
try {
Thread.sleep((int)(1000*Math.random()));
}
catch (InterruptedException e) { }
s=temp;
System.out.println("s="+s);
}
}
class Customer extends Thread
{
public void run()
{
for( int i=1; i<=4; i++)
Cbank.sub(100);
}
}
public class Thread3
{
public static void main(String args[])
{
Customer customer1 = new Customer();
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
實驗2 編程練習
利用多線程及同步方法,編寫一個程序模擬火車票售票系統,共3個窗口,賣10張票,程序輸出結果類似(程序輸出不唯一,可以是其他類似結果)。
Thread-0窗口售:第1張票
Thread-0窗口售:第2張票
Thread-1窗口售:第3張票
Thread-2窗口售:第4張票
Thread-2窗口售:第5張票
Thread-1窗口售:第6張票
Thread-0窗口售:第7張票
Thread-2窗口售:第8張票
Thread-1窗口售:第9張票
Thread-0窗口售:第10張票
代碼:
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
Mythread mythread = new Mythread();
Thread t1 = new Thread(mythread);
Thread t2 = new Thread(mythread);
Thread t3 = new Thread(mythread);
t1.start();
t2.start();
t3.start();
}
}
class Mythread implements Runnable {
int x = 1;
boolean f = true;
public void run() {
while (f) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (this) {
if (x <= 10) {
System.out.println(Thread.currentThread().getName() + "窗口售:第" + x + "張票");
x++;
} else
f = false;
}
}
}
}
Demo
結果:
本周學習總結:
5、線程的創建和啟動
A、[重點]繼承Thread類或實現Runnable接口,重寫或實現run方法,run方法代表線程要完成的任務
B、創建Thread子類或是Runnable的實現類,即創建的線程對象;不同的是接口實現線程,
需要將接口的實現類作為參數傳遞給Thread類的構造參數
C、用線程對象的start方法啟動線程
6、繼承Thread和實現Runnable接口創建線程的區別
采用Runnable接口實現線程:
優勢:
A、線程類只是實現了Runnable接口,還可以繼承其他的類
B、在這種方式下,可以多個線程共享同一個目標對象,所以很合適多個線程來處理同一份資源的情況,
從而可以將CPU、代碼和數據分開,形成清晰的模型,較好的面相對象思想。
劣勢:編程稍微復雜,如果需要訪問當前線程需要用Thread.currentThread方法來獲取
采用繼承Thread類的方式實現線程:
優勢:編寫簡單,如果要獲得當前線程直接this即可
劣勢:線程類繼承了Thread,不能在繼承其他類
相對而言,用Runnable的方式更好,具體可以根據當前需要而定;
7、線程生命周期
線程被創建啟動後,不並不是啟動後就進入了執行狀態,也不是一直處於的執行狀態。
線程的生命周期分為創建(new)、就緒(Runnable)、運行(running)、阻塞(Blocked)、死亡(Dead)五種狀態。
線程啟動後不會一直霸占CPU資源,所以CPU需要在多條線程中切換執行,線程就會在多次的運行和阻塞中切換。
8、新建(new)和就緒(Runnable)狀態
當new一個線程後,該線程處於新建狀態,此時它和Java對象一樣,僅僅由Java虛擬機為其分配內存空間,並初始化成員變量。
此時線程對象沒有表現出任何的動態特征,程序也不會執行線程的執行體。
註意:run方法是線程的執行體,不能由我們手動調用。我們可以用start方法啟動線程,系統會把run方法當成線程的執行體來運行,
如果直接調用線程對象run方法,則run方法立即會被運行。而且在run方法返回之前其他線程無法並行執行,
也就是說系統會把當前線程類當成一個普通的Java對象,而run方法也是一個普通的方法,而不是線程的執行體。
9、運行(running)和阻塞(Blocked)狀態
如果處於就緒狀態的線程就獲得了CPU,開始執行run方法的線程執行體,則該線程處於運行狀態。
單CPU的機器,任何時刻只有一條線程處於運行狀態。當然,在多CPU機器上將會有多線程並行(parallel)執行,
當線程大於CPU數量時,依然會在同一個CPU上切換執行。
線程運行機制:一個線程運行後,它不可能一直處於運行狀態(除非它執行的時間很短,瞬間執行完成),線程在運行過程中需要中斷,
目的是讓其他的線程有運行機會,線程的調度取決於底層的策略。對應搶占式的系統而言,系統會給每個可執行的線程一個小時間段來處理任務,
當時間段到達系統就會剝奪該線程的資源,讓其他的線程有運行的機會。在選擇下一個線程時,系統會考慮線程優先級。
以下情況會出現線程阻塞狀態:
A、線程調用sleep方法,主動放棄占用的處理器資源
B、線程調用了阻塞式IO方法,在該方法返回前,該線程被阻塞
C、線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。
D、線程等待某個通知(notify)
E、程序調用了suspend方法將該線程掛起。不過這個方法容易導致死鎖,盡量不免使用該方法
當線程被阻塞後,其他線程將有機會執行。被阻塞的線程會在合適的時候重新進入就緒狀態,註意是就緒狀態不是運行狀態。
也就是被阻塞線程在阻塞解除後,必須重新等待線程調度器再次調用它。
針對上面線程阻塞的情況,發生以下特定的情況可以解除阻塞,讓進程進入就緒狀態:
A、調用sleep方法的經過了指定的休眠時間
B、線程調用的阻塞IO已經返回,阻塞方法執行完畢
C、線程成功獲得了試圖同步的監視器
D、線程正在等待某個通知,其他線程發出了通知
E、處於掛起狀態的線程調用了resume恢復方法
線程從阻塞狀態只能進入就緒狀態,無法進入運行狀態。而就緒和運行狀態之間的轉換通常不受程序控制,而是由系統調度所致的。
當就緒狀態的線程獲得資源時,該線程進入運行狀態;當運行狀態的線程事情處理器資源時就進入了就緒狀態。
但對調用了yield的方法就例外,此方法可以讓運行狀態轉入就緒狀態。
10、線程死亡(Dead)狀態
線程會在以下方式進入死亡狀態:
A、run方法執行完成,線程正常結束
B、線程拋出未捕獲的異常或Error
C、直接調用該線程的stop方法來結束線程—該方法易導致死鎖,註意使用
註意:當主線程結束的時候,其他線程不受任何影響。一旦子線程啟動後,會擁有和主線程相同的地位,不受主線程影響。
isAlive方法可以測試當前線程是否死亡,當線程處於就緒、運行、阻塞狀態,該方法返回true,如果線程處於新建或死亡狀態就會返回false。
不要試圖對死亡的線程調用start方法,來啟動它。死亡線程不可能再次運行。
11、控制線程
Java線程提供了很多工具方法,這些方法都很好的控制線程
A、join線程
讓一個線程等待另一個線程完成的方法。當某個程序執行流中調用其他線程的join方法時,調用線程將會被阻塞,直到被join方法的join線程執行完成為止。
join方法通常有使用線程的程序調用,將大問題劃分成許多小問題。每個小問題分配一個線程。當所有的小問題得到處理後,再調用主線程進一步操作。
join有三種重載模式:
一、join等待被join的線程執行完成
二、join(long millis)等待被join的線程時間最長為millis毫秒,如果在millis毫秒外,被join的線程還沒有執行完則不再等待
三、join(long millis, int nanos)被join的線程等待時間長為millis毫秒加上nanos微秒
通常我們很少用第三種join,原因有二:程序對時間的精度無需精確到千分之一毫秒
計算機硬件、操作系統也無法做到精確到千分之一毫秒
B、後臺線程
有一種線程,在後臺運行,它的任務是為其他線程提供服務,這種線程被稱為“後臺線程(Daemon Thread)”,有被稱為“守護線程”或“精靈線程”。
JVM的垃圾回收器線程就是後臺進程。
後臺進程有個特征是:如果前臺的進程都死亡,那麽後臺進程也死亡。(它為前臺進程服務)
用Thread的setDaemon (true)方法可以指定當前線程為後臺線程。
註意:前臺線程執行完成死亡後,JVM會通知後臺線程,後臺線程就會死亡。但它得到通知到後臺線程作成響應,需要一段時間,
而且要將某個線程設置為後臺線程,必需要在該線程啟動前設置,也就是說設置setDaemon必需在start方法前面調用。
否則會出現java.lang.IllegalThreadStateException異常
C、線程休眠sleep
如果需要當前線程暫停一段時間,並進入阻塞狀態就需要用sleep,sleep有2中重載方式:
sleep(long millis)讓當前線程暫停millis毫秒後,並進入阻塞狀態,該方法受系統計時器和線程調度器的影響
sleep(long millis, int nanos)讓當前正在執行的線程暫停millis毫秒+nanos微秒,並進入阻塞
當調用sleep方法進入阻塞狀態後,在sleep時間段內,該線程不會獲得執行機會,即使沒有其他可運行的線程,處於sleep的線程不會執行。
D、線程讓步yield
yield和sleep有點類似,它也可以讓當前執行的線程暫停,但它不會阻塞線程,只是將該線程轉入到就緒狀態。
yield只是讓當前線程暫停下,讓系統線程調度器重新調度下。
當yield的線程後,當前線程暫停。系統線程調度器會讓優先級相同或是更高的線程運行。
sleep和yield的區別
(1)、sleep方法暫停當前線程後,會給其他線程執行集合,不會理會線程的優先級。但yield則會給優先級相同或高優先級的線程執行機會
(2)、sleep方法會將線程轉入阻塞狀態,直到經過阻塞時間才會轉入到就緒狀態;而yield則不會將線程轉入到阻塞狀態,它只是強制當前線程進入就緒狀態。
因此完全有可能調用yield方法暫停之後,立即再次獲得處理器資源繼續運行。
(3)、sleep聲明拋出了InterruptedException異常,所以調用sleep方法時,要麽捕獲異常,要麽拋出異常。而yield沒有申明拋出任何異常
E、改變線程優先級
每個線程都有優先級,優先級決定線程的運行機會的多少。
每個線程默認和它創建的父類的優先級相同,main方法的優先級是普通優先級,那在main方法中創建的子線程都是普通優先級。
getPriority(int newPriority)/setPriority(int)
設置優先級有以下級別:
MAX_PRIORITY 值是10
MIN_PRIORITY 值是1
NORM_PRIORITY 值是5
範圍是1-10;
張雲飛 201771010143 《面對對象程序設計(java)》第十七周學習總結