
程序中的定時器功能與我們現(xiàn)實生活中的定時器功能相似,都有起提示作用,但是與現(xiàn)實生活中鬧鐘不同的是,程序里的鬧鐘不僅是提醒,還能真正的去做事情。也就是說它的權限更大,更像是一個機器人,我們給它設定一個時間點讓它去做什么事情,而不是說像鬧鐘一樣,只能提醒我們,但是改變不了我們的想法,到底做不做這件事。
我們以后開發(fā)中,也會經(jīng)常使用到定時器,這是軟件開發(fā)中的一個重要組件。尤其是“網(wǎng)絡編程”,比如說我們訪問一個網(wǎng)頁的話,很容易出現(xiàn)卡的現(xiàn)象,這時我們就可以使用定時器,來進行“止損”。一旦超時,就結(jié)束這次訪問,不再阻塞/等待。
標準庫中的定時器對于定時器,標準庫中提供了一個Timer類,我們可使用這個Timer來做我們想定時做的事情。
Timer類的核心方法是schedule,它包含兩個參數(shù).
第一個參數(shù)是即將要執(zhí)行的任務的代碼,以Runnable接口的形式呈現(xiàn)或者說這個TimerTask這個抽象類實現(xiàn)了Runnable接口,我們只需要繼承這個類重寫它的run方法即可;
第二個參數(shù)是指定多長時間后執(zhí)行,單位為毫秒(millisecond)。
public class Code28_TimerTest {public static void main(String[] args) {Timer timer=new Timer();
System.out.println("已經(jīng)設置好定時器");
timer.schedule(new TimerTask() {@Override
public void run() {System.out.println("執(zhí)行任務1");
}
},1000);
timer.schedule(new TimerTask() {@Override
public void run() {System.out.println("執(zhí)行任務2");
}
},2000);
timer.schedule(new TimerTask() {@Override
public void run() {System.out.println("執(zhí)行任務3");
}
},3000);
}
}![[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hh46HNBk-1673685943980)(F:\typora插圖\image-20230113231532477.png)]](/upload/otherpic35/1312e58f028743eebd0f1218d091f676.jpg)
(一)思路分析
我們已經(jīng)知道怎么使用標準庫中的定時器,下邊我們來自己實現(xiàn)一個定時器。那么在實現(xiàn)定時器之前,我們需要知道定時器需要做什么,才能更好的實現(xiàn)。
1.讓被注冊的任務能夠在指定時間內(nèi)執(zhí)行
2.一個定時器可以注冊多個任務,并且按照時間的先后執(zhí)行
那么,接下來我們想想怎么才能達到這樣的目的。
首先,他需要按照推遲時間的長短存放我們的任務,我們需要一個數(shù)據(jù)結(jié)構存放,不難想到需要隊列,又因為由時間先后來決定先后,所以我們可以采用一個優(yōu)先級隊列,又因為定時器可以在多線程環(huán)境下正常工作,所以我們還需要保證線程安全,所以這里我們最終存儲任務的數(shù)據(jù)結(jié)構就是基于堆實現(xiàn)的阻塞隊列即PriorityBlockingQueue,因為這里使用的是時間戳,所以不需要額外傳比較器,直接創(chuàng)建的就是小根堆.
其次,我們需要一個掃描線程,用來判斷是不是到該執(zhí)行的時間了,確保MyTImer一旦被實例化就能夠這個線程就開始工作,所以,我們需要在這個類的構造方法中創(chuàng)建這個線程(不理解,可以先記住在構造方法中需要創(chuàng)建線程這個點)。
然后,對比原來的Timer,還有一個非常重要的成員方法schedule,用來把用戶設置的任務和時間啥的放進任務隊列,相當于普通隊列的offer功能。
最后,因為線程是搶占執(zhí)行、隨機調(diào)度的,我們這里就通過wait/notify來控制線程的執(zhí)行順序。wait/notify方法的調(diào)用需要一個對象,它的阻塞隊列和我們存放任務的阻塞隊列相互呼應,只不過我們外部看不到。所以我們這里再定義一個私有的Object類對象。
綜上我們的MyTimer={私有Object類型對象+存放任務的阻塞隊列+連接對象阻塞隊列和存放任務隊列的掃描線程}。
具體實現(xiàn)細節(jié)我們在下邊討論
(二)代碼實現(xiàn)
因為我們的任務都是Runnable類型的,與此同時,我們還需要給它配一個時間,所以我們不妨自定義一個MyTask類。因為,任務之間我們是需要排優(yōu)先級的,是可比較的,所以我們需要實現(xiàn)比較器,這里我們采用實現(xiàn)Comparable接口。隨之而來的,我們需要重寫compareTo方法。因為這個任務是以runnable形式存在的,而這個runnable我們又是定義在類中的,它是需要顯式調(diào)用,我們的任務才能工作,所以我們這里需要提供run方法,供外部調(diào)用,啟動任務。
因為我們可以很容易的通過本地方法currentTimeMillis得到當前的時間,但是我們通過記錄每次任務安排時間的時刻,但是這樣做免不了有些麻煩,所以我們不如直接放任務時刻就設置成具體的時間點,也就說我們在schedule時時間在原來的基礎上在加上當前的時間。
最后,我們需要明確wait和notify的位置以及過程的模擬其實也就是線程怎么周期性掃描的問題。
wait、notify的話肯定是locker調(diào)用,然后呢,我們每次去取任務時,如果到時間了,不就直接執(zhí)行了嗎,但是如果不到時間,那么我們需要阻塞等待,所以說,我們的wait就在if邏輯里邊的put(會觸發(fā)堆的調(diào)整)之后。又因為wait其實是和join方法一樣,可以規(guī)定等待的時間,不死等的,那么這個時間定的肯定是現(xiàn)在時間和目標時間的時間差。wait這里就安排好了,記得進行異常處理哦。
那么notify呢?因為上邊如果不到時間的話,線程其實已經(jīng)進入了阻塞等待狀態(tài),這個時候我們新加入的任務如果執(zhí)行時間早于原來等待時間的話,就錯過了,所以,這里我們每次新任務加進來的時候,就進行通知。原來如果在阻塞等待,那么就解除阻塞狀態(tài),如果沒有在等待狀態(tài),空打一槍也沒關系。這樣notify的位置我們也安排好了,記得加鎖。
最后,對于線程安全,我們把讀寫操作捆綁,讓這個操作是原子的。
【一般鎖的范圍我們需要合理控制】
class MyTask implements Comparable{//需要執(zhí)行任務的內(nèi)容
private Runnable runnable;
//推遲的時間
private long delaytime;
public MyTask(Runnable runnable, long delaytime) {this.runnable = runnable;
this.delaytime = delaytime;
}
public long getDelaytime() {return delaytime;
}
@Override
public int compareTo(MyTask o) {return (int)(this.delaytime-o.delaytime);
}
//執(zhí)行任務!!!!
public void run(){runnable.run();
}
}
class MyTimer{//用來控制線程執(zhí)行順序的對象(利用它的阻塞隊列)
private Object locker=new Object();
//用來存放任務的隊列
private PriorityBlockingQueuequeue=new PriorityBlockingQueue<>();
//掃描線程
private Thread t;
public MyTimer(){t=new Thread(){@Override
public void run() {while (true) {try {synchronized (locker){MyTask myTask=queue.take();
long curTime=System.currentTimeMillis();
if(curTime//不到時間,不執(zhí)行,把任務再塞回去
queue.put(myTask);
//這里的等待是最長等待時間
locker.wait(myTask.getDelaytime()-curTime);
}else{myTask.run();
}
}
} catch (InterruptedException e) {throw new RuntimeException(e);
}
}
}
};
t.start();
}
public void schedule(Runnable runnable,long after){MyTask myTask=new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(myTask);
synchronized (locker) {locker.notify();
}
}
}
public class Code29_MyTimer {public static void main(String[] args) {MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {@Override
public void run() {System.out.println("執(zhí)行任務1");
}
},1000);
myTimer.schedule(new Runnable() {@Override
public void run() {System.out.println("執(zhí)行任務2");
}
},2000);
}
} ![[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OpABWJho-1673685943982)(F:\typora插圖\image-20230114004829899.png)]](/upload/otherpic35/603891996f9f46a28d4d66a4a3e123ff.jpg)
當當前代碼能滿足需求的前提下,我們不免想要壓縮時間來提高編程的效率。我們已經(jīng)知道線程是為了解決并發(fā)程度很高的情況下,創(chuàng)建/銷毀進程時間開銷很大的一種優(yōu)化辦法。然而,很多東西需要有對比,當并發(fā)程度進一步提高時,多線程確實要比多進程編程效率要高,但是這跟我們預期的效率還差點意思,所以,為了進一步提高并發(fā)編程下的效率,前輩們提出了一些方法供我們使用。
1.纖程也稱為“輕量級線程”。雖然這種辦法能給并發(fā)編程帶來一系列的優(yōu)勢,但是但是它并沒有被廣泛納入標準庫中,java就位列其中。不過近些年比較火的GO語言,將它納入標準庫了。
2.“線程池”。與字符串常量池、數(shù)據(jù)庫連接池類似的是,線程池也是提前創(chuàng)建好,隨用隨去,不需要反復創(chuàng)建/銷毀,效率會比較高。我們在java中,還是使用線程池比較多一些。
對于它的概念我們不必摳字眼,只需要理解它的意思,知道它大概是在干什么就可以了。
不過,這里邊可能會有一個疑問,為什么從池子中拿和放比反復創(chuàng)建/銷毀效率要高?反映到計算機本身上的解釋又是什么?
標準庫中的線程池 創(chuàng)建一個線程池對此,這里給出一種解釋。
創(chuàng)建線程/銷毀線程都是由操作系統(tǒng)內(nèi)核來做的;而從池子中獲取線程,把線程還到池子里邊,我們自己用代碼實現(xiàn)。
那么問題進一步轉(zhuǎn)化成為了,為什么由OS內(nèi)核做事情速度<用戶直接做這些事情速度呢?
這里,我們不妨來看個例子:銀行管理系統(tǒng)
對于普通用戶來講,他假設正在辦理一個業(yè)務,需要用到身份證復印件,但是呢,他只帶了原件。這個時候,柜員給它提供了兩種選擇:第一,他幫他去他們的后臺復印;第二,用戶自己去大廳里邊復印。這里我們將情況理想化,假設大廳復印位置無限多或者需要復印的用戶無限少,此用戶復印之后無需再排隊。那么此時就意味著用戶直接復印無限快。又因為銀行后臺不可見,我們只是知道柜員拿著原件去復印了,但是它有沒有借此機會去做其他事情或者到底是先做復印這件事還是先復印趁機再做一些其他事情,這些我們都無從得知。但是一般情況下,他們是會的,上廁所或者摸會魚……那么這就意味著,速度會相對慢。
而實際計算機在執(zhí)行線程的任務時,因為操作系統(tǒng)內(nèi)核需要負責的任務比較多,當我們把任務交給它時,其實也就是將任務放到了它的任務隊列里邊,很大概率不能第一時間執(zhí)行。(它不存在摸魚情況,它是一個機器,只是負擔太大,忙不過來)而我們?nèi)绻捎镁€程池,自己取線程,自己放回(其實run方法執(zhí)行完了,執(zhí)行此任務的線程自動解放回歸池子),就很大簡單了操作系統(tǒng)內(nèi)核的負擔。所以這就是為什么OS內(nèi)核做事情速度<用戶直接做這些事情速度。
另外,我們這里解釋一下什么叫做用戶態(tài)什么叫做內(nèi)核態(tài)。整個計算機等價于創(chuàng)建銀行的假設是政府,政府把這個銀行的管理員權限交給了柜員,而操作系統(tǒng)內(nèi)核等價于柜員,剩余的操作系統(tǒng)空間等價于普通用戶。一些操作我們不需要內(nèi)核來做,就可以直接做,而有些必須要更高一級的權限,也就是說我們把部分功能(黑盒)的實現(xiàn)交給了內(nèi)核,OS內(nèi)核把黑盒怎么使用給計算機的其他部分說明了。這個黑盒也可以反映到代碼上就是api。
程序借用api完成完整操作的過程叫做系統(tǒng)調(diào)用,驅(qū)動內(nèi)核完成一些工作。這些黑盒到底是怎么實現(xiàn)的,執(zhí)行效率是快是慢,我們都無法控制,都是由OS內(nèi)核獨立完成的。
所以,相對而言,用戶態(tài)程序的執(zhí)行行為整體是可控的,內(nèi)核態(tài)的執(zhí)行行為整體是不可控的。
java標準庫中也提供了現(xiàn)成的線程池,可以直接使用。但是這里還是有些不同的。下邊我們來討論一下,然后給出測試代碼。
這個被提供的類叫ThreadPoolExecutor,這里我們需要重點掌握它的構造方法的各個參數(shù)的含義,以及submit這個給線程池提交任務的方法。又因為這個類提供的功能過于強大用起來比較麻煩,所以我們一般使用被工廠類Executors包裝過的工廠方法構造線程池。下邊我們結(jié)合測試代碼分析。
//for test
public class Code30_ExecutorSevicePoolTest {public static void main(String[] args) {ExecutorService pool= Executors.newFixedThreadPool(5);
for (int i = 0; i< 5; i++) {int n=i+1;
pool.submit(new Runnable() {@Override
public void run() {System.out.println("在線程池中執(zhí)行任務:"+n);
}
});
}
}
}這里跟其他提供組件的使用略有區(qū)別,這里使用的是Executors這個類的靜態(tài)方法,直接構造出對象來,相當于是把new操作隱藏到靜態(tài)方法里邊了。我們每次可以使用submit方法,將任務以Runnable接口的方式交給線程池。
這樣把new操作隱藏在靜態(tài)方法里邊的方法就是工廠方法。提供工廠方法的類就叫做工廠類。這種設計模式叫做工廠模式。
那么工廠模式有什么作用呢?
盡可能的避免了構造方法上的坑,比如創(chuàng)建坐標點,有笛卡爾坐標系和極坐標兩種體系,這兩個參數(shù)我們一般都設置成double,此時我們試圖通過重載完成任務時,就會發(fā)現(xiàn)不能成功。工廠模式這里就是盡可能的填了java語法上的坑。
需要特別說明的是,我們基本可以認為,設計模式就是為了填語法上的坑。又因為不同的語言語法規(guī)定不同,有些設計模式已經(jīng)融入到語法當中了,所以每個語言上使用的設計模式也不盡相同。
Executors給我們提供了很多種風格的線程池
而這些線程池,本質(zhì)上都是通過包裝ThreadPoolExecutor來實現(xiàn)出來的,而這個線程池用起來比較麻煩,功能更強大。
再有,運行之后,我們發(fā)現(xiàn),main線程雖然結(jié)束了,但是整個進程并沒有結(jié)束,這是因為線程池中的線程都是前臺線程,會阻止進程結(jié)束。定時器中的各個任務也是前臺線程,所以最后并沒有Process finished巴拉巴拉的。
![[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GWi1DDWy-1673685943984)(F:\typora插圖\image-20230114150136720.png)]](/upload/otherpic35/b9c0720530fc4deb80ad5ac4b37c75c4.jpg)
如果我們想要它強制停止,可以點擊右上角的stop按鈕。
另外,這里還涉及到lambda的一個小的語法點——變量捕獲。變量i是main線程中的局部變量,run方法是屬于Runnbale接口的,并不一定是立刻馬上去執(zhí)行,而線程池中帶著任務的線程和主線程基本上是并行的關系,有可能主線程結(jié)束了,它這部分的代碼塊已經(jīng)銷毀了,他們還沒結(jié)束或者在線程池中還沒排到,所以這里再去取一直變化的i是不恰當?shù)模詊ava官方給出了這樣一個語法,如果拿到的變量是不可變的或者final修飾(jdk1.8以后)就可以。所以需要再次定義個中間變量n.這是為了避免變量生命周期的不同帶來的錯誤。
再有,當線程任務耗時是差不多時,基本上可以認為每個線程負責的任務數(shù)是平均的。
ThreadPoolExecutor構造方法解析關于這個簡單的測試代碼我們搞明白了,我們下邊來看重頭戲,ThreadPoolExecutor這個類的構造方法!!!
![[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7MjIpx4l-1673685943984)(F:\typora插圖\image-20230114161339311.png)]](/upload/otherpic35/3ef98f049f71438ba0526d74292f5aac.jpg)
下邊我們來討論一個問題
corePoolSize和maximumPool設置多少合適?
不同的程序特點不同,此時要設置的線程數(shù)也是不同的。考慮兩個極端情況。
然而,實際開發(fā)中沒有程序符合這兩種理想模式,真實的程序,往往是一部分吃cpu,一部分等待io。因此我們需要根據(jù)具體占比進行設置,一般是通過測試的方法。
不難確定,線程池={阻塞隊列=》存放任務+若干工作線程(類似定時器,也是在構造方法中)+注冊任務的submit方法}
class MyThreadPool{//不涉及時間,直接BQ
private BlockingQueuequeue=new LinkedBlockingQueue<>();
//構造方法中創(chuàng)建出工作的線程
public MyThreadPool(int n){for (int i = 0; i< n; i++) {Thread t=new Thread(()->{ while(true){ Runnable runnable= null;
try { runnable = queue.take();
} catch (InterruptedException e) { throw new RuntimeException(e);
}
runnable.run();
}
});
t.start();
}
}
//用來注冊任務的方法
public void submit(Runnable runnable){try {queue.put(runnable);
} catch (InterruptedException e) {throw new RuntimeException(e);
}
}
}
//for test
public class Code31_MyThreadPool {public static void main(String[] args) {MyThreadPool pool=new MyThreadPool(10);
for (int i = 0; i< 98; i++) {int n=i;
pool.submit(new Runnable() {@Override
public void run() {System.out.println("執(zhí)行線程池中的任務"+n);
}
});
}
}
} ![[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sch3sBsr-1673685943985)(F:\typora插圖\image-20230114163745054.png)]](/upload/otherpic35/86cd783ae4d34074b81674ec9bf26035.jpg)
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧
本文標題:JavaEE|多線程代碼實例之定時器與線程池-創(chuàng)新互聯(lián)
本文網(wǎng)址:http://www.chinadenli.net/article26/dcdpcg.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、面包屑導航、Google、搜索引擎優(yōu)化、網(wǎng)站設計、ChatGPT
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)