目錄
1、JUC(java.util.concurrent)的常見類
1.1、Callable接口的用法(創(chuàng)建線程的一種寫法)
?1.2、ReentrantLock可重入互斥鎖
1.2.1、ReentrantLock和synchronized的區(qū)別?
1.2.2、如何選擇使用哪個鎖
1.3、Semaphore信號量
1.4、CountDownLatch
?2、線程安全的集合類
2.1、多線程環(huán)境使用ArrayList
?2.2、多線程使用隊列
2.3、多線程使用哈希表
2.3.1、HashTable和ConcurrentHashMap的區(qū)別
1、JUC(java.util.concurrent)的常見類
JUC就是取java.util.concurrent的三個單詞的首字母。所以JUC中存放的就是Java多線程開發(fā)使用到的工具類。
1.1、Callable接口的用法(創(chuàng)建線程的一種寫法)
- Callable接口非常類似于Runnable接口,Runnable接口通過run方法描述一個任務,表示一個線程要干啥,但是run方法的返回值類型是void,不能返回一個任務的結果產出。
- 而Callable方法是通過重寫call()方法,來描述一個線程執(zhí)行的任務,在完成結果之后,可以返回一個計算結果。
?這里我們通過一個代碼來了解Callable接口
創(chuàng)建線程計算1+2+3+.....+1000,使用Callable版本
- 創(chuàng)建一個匿名內部類,實現Callable接口,Callable帶有泛型參數,泛型參數表示返回值的類型
- 重寫Callable的call方法,完成累加的過程,直接通過返回值返回計算結果。
- 把callable實例使用FutuerTask包裝一下
- 創(chuàng)建線程,線程的構造方法傳入FutureTask,此時新線程就會執(zhí)行FutureTask內部的Callable的call方法,完成計算,計算結果就放到FutureTask對象中。
- 在主線程中調用futureTask.get()能夠阻塞等待新線程計算完畢,并獲取到FutureTask中的結果。
public class TestDemo27 { public static void main(String[] args) throws ExecutionException, InterruptedException { //創(chuàng)建一個任務 Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i < 1000; i++) { sum += i; } return sum; } }; //創(chuàng)建一個線程,來執(zhí)行第一個任務 //Thread構造方法 不能直接將callable對象作為參數,需要使用FutureTask類進行包裝一下,將FutureTask對象作為參數傳給Thread。 FutureTask<Integer> futureTask = new FutureTask<>(callable); Thread t = new Thread(futureTask); t.start(); System.out.println(futureTask.get()); } }
?我們這里來理解一下FutureTask類的作用。
我們去餐館吃飯,在我們將菜點了之后,服務員給后廚大廚一張小票,也給我們一張小票。讓后廚大廚根據小票上的要求制作,讓我們通過小票去領我們自己的飯。我們使用的FutureTask就相當于一個小票,我們此時將futureTask傳給t線程,就相當于大廚通過小票知道他要怎樣做。我們通過futureTask.get()獲取計算出來的結果,也就是我們的飯。
???在上述的代碼中,執(zhí)行任務在t線程,而獲取任務執(zhí)行結果在主線程,這怎么能夠確定多線程執(zhí)行時,t線程一定在主線程之前結束??
???我們在主線程中futureTask調用get方法,這個get方法,就有相當于join的作用,他會阻塞等待t線程執(zhí)行完畢,再去執(zhí)行主線程中的get方法。
?總結Callable
- Callable和Runnable相對,都是描述一個"任務"。Callable描述的是帶有返回值的任務,Runnable描述的是不帶返回值的任務。
- Callable通常需要搭配FutureTask來使用。FutureTask用來保存Callable的返回值結果,因為Callable往往是在另一個線程中執(zhí)行的,啥時候執(zhí)行完并不確定。
- FutureTask就可以負責這個等待結果出來的工作。
?1.2、ReentrantLock可重入互斥鎖
ReentranLock這是鎖的另一種實現方式,和synchronized定位類似,都是用來實現互斥效果,保證線程安全。
?ReentrantLock的用法:
- lock():加鎖,如果獲取不到鎖就死等。
- trylock(超時時間):加鎖,如果獲取不到鎖,等待一定的時間之后就會放棄加鎖。
- unlock():解鎖
1.2.1、ReentrantLock和synchronized的區(qū)別?
- synchronized是一個關鍵字,是JVM內部實現的(大概率是基于C++實現),ReentranLock是標準庫中的一個類,在JVM外實現的(基于Java實現)
- synchronized使用時不需要手動釋放鎖,ReentrantLock使用時需要手動釋放,使用起來更靈活,但是也容易遺漏unlock
- synchronized在申請鎖的失敗時,會死等。ReentrantLock可以通過trylock的方式等待一段時間就放棄。(讓程序員更靈活的決定接下來咋做)
- synchronized是非公平鎖,ReentrantLock默認是非公平鎖,但是它提供了公平和非公平兩種工作模式,可以通過構造方法傳入一個true開啟公平鎖模式。
- 更強大的喚醒機制,synchronized是通過Object的wait/notify實現等待-喚醒,每次喚醒的是一個隨機等待的線程,ReentrantLock搭配Condition類實現等待-喚醒。Condition這個類也能起到等待通知的效果,可以更精確控制喚醒某個指定的線程。
1.2.2、如何選擇使用哪個鎖
- 鎖競爭不激烈的時候,使用synchronized,效率更高,自動釋放更方便。
- 鎖競爭激烈的時候,使用ReentrantLock,搭配trylock更靈活的控制加鎖的行為,而不是死等。
- 如果需要使用公平鎖,使用ReentrantLock.
1.3、Semaphore信號量
信號量:用來表示"可用資源的個數"。本質上就是一個計數器。
?理解信號量
- 可以把信號量想象成是停車場的展示牌:當前有車位100個,表示有100個可用資源。
- 當有車開進去的時候,就相當于申請一個可用資源,可用車位就-1(這個稱為信號量的P操作)
- 當有車開出來的時候,就相當于釋放一個可用資源,可用車位就+1(這個稱為信號量的V操作)
- 如果計數器的值已經為0了,還嘗試申請資源,就會阻塞等待,直到其他線程釋放資源。
Semaphore的PV操作中的加減計數器操作都是原子的,可以在多線程環(huán)境下直接使用。
- 我們所說的鎖,本質上是計數器為1的信號量,可用資源只有做一個,取值只有1和0兩種,也叫做二元信號量。?一個線程獲取到鎖,這個時候信號量為0,只有等到線程將該鎖釋放掉,這個時候信號量為1,其他線程才能獲取到鎖。
- 我們可以認為信號量是更廣義的鎖,他不僅能管理鎖,這中非0即1的資源;也能管理多個資源。
1.4、CountDownLatch
- 同時等待N個任務執(zhí)行結束。
- 就好比跑步比賽,6個選手依次就位,發(fā)令槍一響,就表示開始,當最后一個人沖過終點,才能公布成績。
?將上面的情況可以使用多線程的思路進行描述
- 主線程,創(chuàng)建10個線程。主線程創(chuàng)建一個CountDownLatch對象,構造方法參數寫10(表示10個參賽選手),10個線程分別完成各自的任務。
- 主線程使用CountDownLatch.await方法,來阻塞等待所有線程都執(zhí)行完任務。
- 10個線程每個線程執(zhí)行完,都會調用一個CountDownLatch.countDown方法(表示選手到達終點)
- 10個線程在調用countDown方法時,主線程調用的await方法會記錄有幾個線程調用了countDown方法(就相當于,裁判員在記錄有幾個選手已經過線了),當這10個線程都調用過countDown方法之后,此時主線程的await就會阻塞接觸,接下來就可以進行后續(xù)工作了。
?2、線程安全的集合類
我們在數據結構中說到的ArrayList、LinkedList、HashMap、PriorityQueue都是線程不安全的集合類。在多線程環(huán)境下使用,有可能會出現問題。
?這些數據結構多線程不安全,但是還要使用,該做怎樣的處理呢?
2.1、多線程環(huán)境使用ArrayList
1??最直接的方法,就是使用鎖(synchronized或ReentrantLock),手動保證.
多個線程去修改ArrayList此時就可能有問題,就可以給修改操作進行加鎖。
2??、可以使用Vector類來代替ArrayList類。
Vector類中的關鍵方法都是帶有synchronized的,這樣可以保證在多線程環(huán)境下,這個類是安全的。但是Java官方明確表示,將Vector這個類標記為不建議使用的類。
3??、?使用collections.synchronizedList(new list集合類)
- collections.synchronizedList它就相當于一個外殼,將我們想要使用的list集合類,放在它里面,讓list集合類當中的關鍵操作都帶上synchronized。
- synchronizedList是標準庫提供的一個基于synchronized進行線程同步的List.
- synchronizedList的關鍵操作上都帶有synchronized
4??、 使用CopyOnWriteArrayList(支持"寫時拷貝"的集合類)
CopyOnWrite容器即寫時復制的容器。
- 當我們往一個容器里添加元素的時候,不直接往當前容器中添加,而是先將當前容器進行Copy,復制出一個新的容器,然后往新的容器里添加元素。
- 添加完元素之后,在將原容器的引用指向新的容器。(引用的賦值操作,本身就是原子的)
所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫是不同的容器。
多線程讀ArrayList是,此時沒有線程安全的問題,但是當一些線程讀,一些線程修改的時候,就會出現線程安全問題,但是使用CopyOnWriteArrayList,就不會產生線程安全問題了,讀和寫相互不影響。
- 優(yōu)點:這樣做的好處就是,修改的同時對于讀操作,是沒有任何影響的,讀的時候就會讀取原來的舊數據,不會出現,讀一個帶有"修改了一半"的中間版本,也就是說適合于讀多寫少的情況,也適合數據小的情況,在我們日常配置數據的時候,經常就會用到這類操作。這種策略也叫做"雙緩沖區(qū)策略"。就像我們在打游戲的時候,顯卡就是采用的這種方式,顯示器在讀前一幀的畫面的時候,顯卡在畫下一幀的畫面。讀的時候,在舊的集合中讀,寫的時候在新的集合中寫,兩種不會產生影響。
- 缺點:占用內存較多,新寫的數據不能第一時間讀取到。
?2.2、多線程使用隊列
我們之前說過的BlockingQueue就是線程安全的,在之前線程池的博客中已經說到了,這里就不過多說明了。
2.3、多線程使用哈希表
HashMap本身不是線程安全的。
??在多線程環(huán)境下使用哈希表可以使用:
1??HashTable(雖然線程安全,但是不建議使用)
HashTable只是簡單的把關鍵方法加上了synchronized關鍵字。
2??ConcurrentHashMap(建議使用)
2.3.1、HashTable和ConcurrentHashMap的區(qū)別
1??加鎖粒度的不同(觸發(fā)鎖沖突的頻率)
HashTable是針對整個哈希表加鎖,任何的增刪改查操作,都會觸發(fā)加鎖,也就都會可能有鎖競爭。
??我們通過下面的場景來展現HashTable出現的問題。
??此時我們通過下面的場景來展現ConcurrentHashMap在遇到與HashTable相同的問題時,它的處理方式,以及優(yōu)點。
?
???補充:
上述情況是從Java1.8開始的,在Java1.7及其之前,ConcurrentHashMap使用"分段鎖",目的和上述類似,相當于是好幾個鏈表共用一把鎖(這個設定,不科學,效率不夠高,代碼寫起來也比較麻煩)
2??ConcurrentHashMap更充分的利用了CAS機制(無鎖編程),比如獲取或更新元素個數,就可以直接使用CAS完成,不必加鎖。
3??優(yōu)化了擴容策略
??對于HashTable,如果元素太多,就會涉及到擴容,擴容需要重新申請內存空間,搬運元素(把元素從舊的哈希表上刪除,插入到新的哈希表上)。如果舊的HashTable中的元素非常多,搬運一次,成本就很高。剛好給HashTable中插入(put)元素的時候,負載因子超過了閾值,一次性搬運全部數據就會導致put操作非常的卡頓。文章來源:http://www.zghlxwxcb.cn/news/detail-469319.html
??對于ConcurrentHashMap擴容的策略,是化整為零,它不會試圖依次性的把所有的元素都搬運到新表當中去,而是每次搬運一部分。文章來源地址http://www.zghlxwxcb.cn/news/detail-469319.html
- 當put觸發(fā)擴容,此時就會直接創(chuàng)建更大的內存空間,但是并不會直接把所有元素都搬運過去,而是值搬運一小部分,這個時候的搬運速度就會比較快。
- 此時就相當于存在兩份hash表了,此時插入元素操作,就會直接往新表中插入元素;刪除元素,就會刪除舊表當中的元素;查找元素,就會新表和舊表一起都查。并且每次操作過程中,都搬運一部分元素,直至最后搬運完成。
到了這里,關于【JavaEE】JUC(java.util.concurrent)的常見類以及線程安全的集合類的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!