問題:--------------------------------------------------------------------------------用戶反饋一些定時(shí)活動(dòng)提前開啟或者延后開啟1) 登錄服務(wù)器,查看時(shí)間確實(shí)慢了或者快了。總之是有幾臺(tái)服務(wù)器時(shí)間不準(zhǔn)確了。2) 查看代碼是使用的ScheduledExecutorService.scheduleAtFixedRate,java的API,不至于這里存在Bug3) 查看log4j日志輸出發(fā)現(xiàn): 12點(diǎn)的定時(shí)活動(dòng),之前的[活動(dòng)運(yùn)行時(shí)間]就是12點(diǎn)整;后面有幾天的[活動(dòng)運(yùn)行時(shí)間]是12點(diǎn)零幾分,而且分秒都一致 確認(rèn)了一下,變化之間同步了一下服務(wù)器時(shí)間,但是沒有重啟jvm4) 初步懷疑是ScheduledExecutorService內(nèi)部執(zhí)行使用的是相對(duì)時(shí)間,不是每次采樣服務(wù)器系統(tǒng)時(shí)間
問題確認(rèn)-測試:--------------------------------------------------------------------------------5) 測試
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);service.scheduleAtFixedRate(new Runnable() { // Runnable-1 @Override public void run() { System.out.PRintln( String.format("/n#### %s ####", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date()))); }}, 0, 10, TimeUnit.SECONDS); // 10秒執(zhí)行一次service.scheduleAtFixedRate(new Runnable() { // Runnable-2 int i = 0; @Override public void run() { System.out.print( (i++) + "," ); }}, 0, 1, TimeUnit.SECONDS); // 1秒執(zhí)行一次輸出-1: 0, #### 2014-11-28 16:51:48.118 #### 1,2,3,4,5,6,7,8,9, #### 2014-11-28 16:51:58.93 #### 10,11,12,13,14,15,16,17,18,19,20, #### 2014-11-28 16:52:08.94 #### 21,22,23,24,25,26,27,28,29, #### 2014-11-28 16:52:18.93 #### 30,31,32,33,34,35,36,37,38,39, #### 2014-11-28 16:52:28.93 #### 40,41,42,43,44,45,46,47,48,49, #### 2014-11-28 16:58:36.480 #### // 調(diào)整時(shí)間 50,51,52,53,54,55,56,57,58,59, #### 2014-11-28 16:58:46.480 #### 60,61,62,63,64,65,66,67,68,69, #### 2014-11-28 16:58:56.480 ####
在 16:52:28.93 時(shí)調(diào)整時(shí)間為16:58:36.480(向后跳), Runnable-2依然進(jìn)行了10次輸出,然后Runnable-1輸出1次
輸出-2: 0, #### 2014-11-28 17:12:40.971 #### 1,2,3,4,5,6,7,8,9, #### 2014-11-28 17:12:50.943 #### 10,11,12,13,14,15,16,17,18,19, #### 2014-11-28 17:13:00.943 #### // 調(diào)整時(shí)間 20,21,22,23,24,25,26,27,28,29, #### 2014-11-28 17:05:09.69 #### 30,31,32,33,34,35,36,37,38,39, #### 2014-11-28 17:05:19.68 ####
在 17:13:00.943 時(shí)調(diào)整時(shí)間為17:05:09.69(向前跳), Runnable-2依然進(jìn)行了10次輸出,然后Runnable-1輸出1次
測試結(jié)論: 時(shí)間的跳躍不影響 Runnable-1 10個(gè)秒單位輸出一次, ScheduledExecutorService 沒有使用系統(tǒng)時(shí)間
問題確認(rèn)-JDK源碼:-------------------------------------------------------------------------------- 初始化
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); new ScheduledThreadPoolExecutor(corePoolSize) super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue())
注冊(cè)定時(shí)任務(wù)
service.scheduleAtFixedRate(new Runnable() {...}) RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Object>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period))); // ScheduledThreadPoolExecutor.ScheduledFutureTask delayedExecute(t); prestartCoreThread addIfUnderCorePoolSize addThread Worker w = new Worker(firstTask); // ThreadPoolExecutor.Worker Thread t = threadFactory.newThread(w); workers.add(w); super.getQueue().add(command); // DelayedWorkQueue執(zhí)行
ThreadPoolExecutor.Worker.run task = getTask() r = workQueue.take(); // DelayedWorkQueueScheduledThreadPoolExecutor.DelayedWorkQueue.take dq.take(); // DelayQueue<RunnableScheduledFuture>
DelayQueue<E extends Delayed> Delayed 元素的一個(gè)無界阻塞隊(duì)列,只有在延遲期滿時(shí)才能從中提取元素。 該隊(duì)列的頭部 是延遲期滿后保存時(shí)間最長的 Delayed 元素。 如果延遲都還沒有期滿,則隊(duì)列沒有頭部,并且 poll 將返回 null。 當(dāng)一個(gè)元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一個(gè)小于等于 0 的值時(shí),將發(fā)生到期。 即使無法使用 take 或 poll 移除未到期的元素,也不會(huì)將這些元素作為正常元素對(duì)待。 例如,size 方法同時(shí)返回到期和未到期元素的計(jì)數(shù)。此隊(duì)列不允許使用 null 元素。 take() long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay > 0) { long tl = available.awaitNanos(delay); }ScheduledThreadPoolExecutor.ScheduledFutureTask.getDelay public long getDelay(TimeUnit unit) { return unit.convert(time - now(), TimeUnit.NANOSECONDS); } final long now() { /** * public static long nanoTime() * 返回最準(zhǔn)確的可用系統(tǒng)計(jì)時(shí)器的當(dāng)前值,以毫微秒為單位。 * 此方法只能用于測量已過的時(shí)間,與系統(tǒng)或鐘表時(shí)間的其他任何時(shí)間概念無關(guān)。 * 返回值表示從某一固定但任意的時(shí)間算起的毫微秒數(shù)(或許從以后算起,所以該值可能為負(fù))。 */ return System.nanoTime() - NANO_ORIGIN; }問題確認(rèn)-nanoTime測試:--------------------------------------------------------------------------------
new Thread(){ public void run () { long lastNanos = System.nanoTime(); long lastMilis = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { try { Thread.sleep(10000L); } catch (InterruptedException e) { } // 10秒鐘輸出一次 long nanos = System.nanoTime(); long millis = System.currentTimeMillis(); System.out.println( String.format("%-25s %-10s %s-%s = %s [%s]", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date(millis)), millis - lastMilis, nanos, lastNanos, nanos - lastNanos, (nanos - lastNanos) / 1000 / 1000 )); lastNanos = nanos; lastMilis = millis; } }}.start();輸出: 2014-12-05 10:10:45.950 10001 107273041687944-107263041775958 = 9999911986 [9999] 2014-12-05 10:10:55.997 10047 107283088127723-107273041687944 = 10046439779 [10046] 2014-12-05 10:20:04.198 548201 107293088578057-107283088127723 = 10000450334 [10000] 2014-12-05 10:00:12.399 -1191799 107303089035855-107293088578057 = 10000457798 [10000] 2014-12-05 10:00:22.399 10000 107313089495208-107303089035855 = 10000459353 [10000] 在 10:10:55.997 時(shí)調(diào)整時(shí)間為10:20:04.198(向后跳), 在 10:20:04.198 時(shí)調(diào)整時(shí)間為10:00:12.399(向前跳), nanos - lastNanos總是維持在 10s左右, millis - lastMilis 確實(shí)期望中的時(shí)間
想法與目標(biāo):--------------------------------------------------------------------------------7) 我想做一個(gè)定時(shí)器,依賴于當(dāng)前系統(tǒng)時(shí)間的定時(shí)器;順便解決當(dāng)前問題 a) 它不是定時(shí)任務(wù),不是一個(gè)任務(wù)系統(tǒng)。 b) 它只做一件事情,到時(shí)間了提醒我做某個(gè)任務(wù)。 比如,12點(diǎn)了,提醒我該吃叫花雞了;20點(diǎn)了,該打幫會(huì)戰(zhàn)了。
初步設(shè)計(jì):--------------------------------------------------------------------------------
8) 初步設(shè)計(jì) a) 首先創(chuàng)建一個(gè)任務(wù)線程池,用于執(zhí)行定時(shí)器叫醒的任務(wù)。 Executors.newScheduledThreadPool(workServicePoolSize) b) 創(chuàng)建一個(gè)定時(shí)器線程, 每隔1秒執(zhí)行一次handleFunc(心跳步長1秒) Executors.newSingleThreadScheduledExecutor c) handleFunc根據(jù)當(dāng)前系統(tǒng)時(shí)間查找到時(shí)的任務(wù),把任務(wù)放置到任務(wù)線程池,由任務(wù)線程池執(zhí)行 execute(new Runnable(){}); 可以初步解決任務(wù)依賴系統(tǒng)時(shí)間來執(zhí)行問題
詳細(xì)設(shè)計(jì):--------------------------------------------------------------------------------
9) linux系統(tǒng)時(shí)間會(huì)變快或者變慢,比如23點(diǎn)戰(zhàn)力排行榜截止并在30分鐘后開始領(lǐng)取獎(jiǎng)勵(lì) a) 如果快的太多;大家當(dāng)前都是22:55,但是服務(wù)器已經(jīng)23:00,想等著最后沖榜的兄弟立馬就哭了 b) 如果慢的太多;大家當(dāng)前都是23:00,但是服務(wù)器才是22:55,我都休息了,準(zhǔn)備開始領(lǐng)取獎(jiǎng)勵(lì),你還能沖擊戰(zhàn)力榜 a) handleFunc每次執(zhí)行后,記錄一下當(dāng)前執(zhí)行時(shí)間為 lastExecuteTime b) handleFunc下次執(zhí)行的時(shí)候,拿 executeTime(當(dāng)前時(shí)間) 和 lastExecuteTime比較一下 如果 executeTime == lastExecuteTime + 1: (心跳步長1秒) 正常時(shí)間,正常執(zhí)行 如果 executeTime > lastExecuteTime + 1: 時(shí)間快了(通常是服務(wù)器時(shí)間慢了;校正服務(wù)器時(shí)間,服務(wù)器時(shí)間會(huì)快進(jìn)), 需要處理一下: [lastExecuteTime + 1, executeTime - 1]的任務(wù) 根據(jù)業(yè)務(wù)決定是否需要立馬補(bǔ)執(zhí)行 [executeTime]的任務(wù) 是當(dāng)前時(shí)間正常任務(wù),需要正常執(zhí)行 如果 executeTime <= lastExecuteTime: 時(shí)間慢了(通常是服務(wù)器時(shí)間快了;校正服務(wù)器時(shí)間,服務(wù)器時(shí)間會(huì)回退): [executeTime, lastExecuteTime] 都執(zhí)行過了,一般不需要再執(zhí)行了
注1:當(dāng)前時(shí)間和lastExecuteTime都是抹去毫秒的,日常定時(shí)服務(wù)基于秒來計(jì)算足夠了 注2: 服務(wù)器可以每隔1個(gè)小時(shí)同步一次時(shí)間,比方 NN:38,通常要避開整點(diǎn)、半點(diǎn)、整十分 注3: 每小時(shí)同步一次最多誤差幾秒而已,對(duì)于普通業(yè)務(wù)而言: 時(shí)間快了的情況下,立馬補(bǔ)執(zhí)行一下就可以了,比較重要的獎(jiǎng)勵(lì)提前或者延遲5秒發(fā)沒有多少差別 時(shí)間慢了的情況下,可以忽視掉[executeTime, lastExecuteTime]間的任務(wù),不需要再執(zhí)行一次了 注4: 執(zhí)行時(shí)間粒度比較小的,比方說1秒執(zhí)行一次的,可以無視時(shí)間跳躍的問題
詳細(xì)設(shè)計(jì)-定時(shí)任務(wù):--------------------------------------------------------------------------------
10) 這樣用定時(shí)器的方案,可以解決時(shí)間跳躍的問題;但是日常開發(fā)通常是定時(shí)周期任務(wù) 比如, 12點(diǎn)吃叫花雞,12點(diǎn)定時(shí)器通知吃叫花雞;但是吃叫花雞是每天12點(diǎn)都吃,這就是個(gè)定時(shí)周期任務(wù),需要每天12點(diǎn)都 通知一下吃。處理方案可以如下: a) 修改“handleFunc根據(jù)當(dāng)前系統(tǒng)時(shí)間查找到時(shí)的任務(wù)” b) 查找的任務(wù)倉庫分為兩類倉庫: 一次性任務(wù)倉庫: 到時(shí)間就執(zhí)行任務(wù),并移除; 任務(wù)倉庫的存儲(chǔ)的key是任務(wù)執(zhí)行時(shí)間戳 每日的任務(wù)倉庫: 任務(wù)倉庫的存儲(chǔ)的key是,任務(wù)相對(duì)于凌晨00:00:00的秒數(shù) handleFunc執(zhí)行時(shí)候,查看一下,今天過去了多少秒(這個(gè)是使用系統(tǒng)時(shí)間),找到對(duì)應(yīng)任務(wù),然后執(zhí)行。這個(gè)不需要移除任務(wù)
這樣依然是定時(shí)器的概念,“12點(diǎn)到時(shí)間了,我叫你去吃叫花雞;明天12點(diǎn)到時(shí)間了,我再叫你去吃叫花雞”, 而不是“12點(diǎn)了,我叫你去吃叫花雞;24小時(shí)后我再叫你去吃雞”。
注1: 同理可以增加每周、每小時(shí)、每分鐘的任務(wù)倉庫 注2: 通常不需要每月、每年、每十年等周期任務(wù),如果需要加也很簡單 注3: 每秒的,不需要這么多少事情, handleFunc 過了就直接執(zhí)行(每次時(shí)間間隔是利用nano計(jì)算出來的1秒,所以應(yīng)該執(zhí)行)
接口設(shè)計(jì):--------------------------------------------------------------------------------
11) 對(duì)外接口: a) 啟動(dòng) b) 關(guān)閉 c) 注冊(cè)一次性任務(wù) d) 注冊(cè)每周任務(wù) e) 注冊(cè)每日任務(wù) f) 注冊(cè)每時(shí)任務(wù):每個(gè)小時(shí)的第幾分、第幾秒執(zhí)行的什么任務(wù) g) 注冊(cè)每分任務(wù) h) 注冊(cè)每秒任務(wù)
其它說明:--------------------------------------------------------------------------------
12) 其它: a) handleFunc 只是找到任務(wù)把它扔進(jìn)工作線程池執(zhí)行,不怎么占用CPU,不會(huì)造成任務(wù)選取的堵塞 b) handleFunc 每次都會(huì)去 分鐘的任務(wù)倉庫查找合適的任務(wù)并執(zhí)行;同一任務(wù)上一分鐘沒執(zhí)行玩,當(dāng)前任務(wù)也會(huì)繼續(xù)執(zhí)行,不會(huì)延遲,會(huì)同時(shí)執(zhí)行 既然是每分鐘任務(wù),任務(wù)不應(yīng)該超過1分鐘;如果偶爾會(huì)超過1分鐘,可以在注冊(cè)的任務(wù)里面自行加鎖 c) 服務(wù)器時(shí)間慢了;校正服務(wù)器時(shí)間,服務(wù)器時(shí)間會(huì)快進(jìn),補(bǔ)執(zhí)行任務(wù)只補(bǔ)處理一定時(shí)間(比如30分鐘) 系統(tǒng)時(shí)間是1小時(shí)同步一次,誤差最大不過幾秒;如果再大,就應(yīng)該升級(jí)內(nèi)核或者換服務(wù)器了 補(bǔ)執(zhí)行的時(shí)間段過程,可能會(huì)影響正常服務(wù)(正常服務(wù)進(jìn)程、系統(tǒng)資源占用等等) d) 任務(wù)扔到線程池里面時(shí)候,會(huì)額外catch住,防止掛掉當(dāng)前線程
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注