定時器
定時器時我們日常開發中會用到的組件工具,類似于一個"鬧鐘",設定一個時間,等到了時間,定時器最自動的去執行某個邏輯,比如博客的定時發布,就是使用到了定時器
Java標準庫里面也提供了定時器的實現
定義一個timer,添加多個任務,每個任務都帶有一個時間定義任務的時候可以使用lambde表達式嗎?
- 答案是不能的,lambde只能用于創建函數式接口的實例,如果非要用lambde表達式創建一個類的實例的話,可以用lambde先創建出一個函數式接口的實例,再把這個函數式接口賦值給類的變量,如下圖,我們的源代碼里面Timer就是一個實現了Runnable函數式接口的一個抽象類,是不可以使用lambde表達式的
內置了前臺線程
我們上圖代碼的執行結果如下
我們發現,控制臺打印了我們的三個任務以后,并沒有進程結束的提示,說明我們的進程并沒有結束,原因是Timer里面內置了前臺線程,它會阻止進程的結束但是我們往timer里面添加的任務都執行完了,也不會結束嗎?
- 因為我們的timer也不知道你是否還會添加新任務進來,所以它不能結束,必須嚴陣以待,也就是不能結束,于是內置了前臺線程
但是就沒有辦法讓timer結束嗎?
- timer里面有一個cancel方法,可以手動調用來結束進程
上述代碼我們調用了cancel后,進程會結束(如下圖)
需要主動調用cancel讓線程主動結束,要不然Timer不知道是否還有其他地方要添加任務的
定時器的實現
我們先思考一下,實現一個定時器需要哪些的內容
- 首先我們需要一個線程,幫助我們掐算時間,時間一到的話,這個線程就會執行該任務
- 其次我們需要一個容器,能夠保存schedule進來的任務
我們直觀的來想的話,我們這個線程就要不斷的遍歷我們這個容器,看看任務的時間是否到了,如果到了,就執行這個任務
但是如果我們容器里面的元素很多呢?要是遍歷的話,時間復雜度就是o(n)了,開銷就很大了
我們此時就需要用到優先級隊列了,我們的每個任務都有實現,先執行時間小的,后執行時間大的,有了優先級隊列,必須是小根堆,隊首元素就是執行時間最小的元素,我們每次只需要看一下隊首元素是否到時間了,要是隊首元素也沒有到時間的話,其他的任務一定沒有到時間我們現在的優先級隊列有兩個選擇:
- PriorityQueue(線程不安全)
- PriorityBloskingQueue(線程安全)
雖然PriorityBloskingQueue是線程安全的,但是我們這里要使用 PriorityQueue,使用PriorityBloskingQueue不太好控制,容易出問題,我們這里手動給PriorityQueue加鎖即可
上面是我們寫的一個類,用來描述一個任務我們這個類直接實現Runnable也是可以的,我們這里是讓這個類持有了Runnable
而且我們這個任務類是需要將其放到優先級隊列里面的,所以要求我們這個類是可以比較的,我們這里也實現了Comparable接口,并且重寫了CompareTo方法,這里的比較的規則就是時間的大小
如果是TreeSet和TreeMap的話我們要求元素是可以比較的,我們就需要實現Comparable和Comparator接口
如果是HsahSet和HashMap的話,就要求元素是可以比較相等的和可哈希的,這時候就要equals和hashCode方法了,有時候為了讓hsah更加高效的話,需要重寫這兩個方法
這是我們寫的構造方法,在這里面我們創建了一個線程,這個線程就是我們的判斷和執行任務的線程
加鎖
由于我們的加入任務的操作(如下圖)和我們的執行并且刪除任務的操作都是對同一個隊列進行操作,可能會有線程安全問題
可以發現我們都給這兩個方法加上鎖了
當我們線程如果發現隊列里面是空的,locker.wait就會釋放鎖,于是scedule就可以獲取到鎖了,獲取到鎖之后就可以往隊列里面添加任務,添加完任務之后,notify就可以將線程喚醒
我們紅框部分的地方也要用到wait,是有時間限制的wait,這里有兩種情況:
- 我們能進入else就說明還沒有到首隊列元素的執行時間,此時我們需要等待(任務執行時間-當前時間)這么多的時間,等到了我們wait的時間,wait就會自動喚醒,再次進入循環取出隊首元素,發現到時間了,于是就執行任務,并且將任務從隊列里面刪除
- 我們進入else,執行wait釋放鎖并且阻塞的時候,此時又有一個任務通過schedule進入到了隊列,schedule里面的notify就會將我們的線程喚醒,線程喚醒之后又進入循環,由于我們加入了新的任務,這個新任務的時間有可能比我們原來的隊首任務的時間小,也有可能大,這時我們的隊首任務就會發生變化,于是又peek一下,取隊首元素,看看是否到了時間,在根據條件執行下面的邏輯
如果我們else里面什么都不加的話,只有一個coontinue,這時我的CPU只是在忙等,雖然在等,但是CPU很忙,因為此時線程的情況是進入了死循環,不如將CPU的資源讓出來,給其他的線程使用,這就是我們wait的另一個作用
使用sleep可以嗎?
不可以,sleep雖然也可以實現等待的結果,但是sleep沒有釋放鎖,如果我們中間有一個schedule又加入了一個任務,這個任務的執行時間是10:20:00,比如現在是10:00:00,我們的首隊列元素的執行時間是10:30:00,此時我們進入else執行到了sleep,按照我們上面wait的代碼,我們這里要sleep30分鐘,這個過程中,鎖沒有被釋放,是被判斷的線程持有的,這就導致我們的schedule方法獲取不到鎖,無法將新的任務添加到隊列,等sleep到了時間被喚醒的時候,已經過了10:20:00了,這個任務就錯過了
執行過程詳解
執行到這里以后我們會進入到漫長的3s的等待,3s結束之后,wait就自動喚醒了