此篇文章與大家分享多線程專題的最后一篇文章:關于JUC常見的類以及線程安全的集合類
如果有不足的或者錯誤的請您指出!
3.JUC(java.util.concurrent)常見的類
3.1Callable接口
Callable和Runnable一樣,都是用來描述一個任務的
但是區(qū)別在于 ,用Callable描述的任務是有返回值的,而通過Runnable描述的任務是沒有返回值的(即run方法的返回值是void)
通過Runnable,要想獲取到"返回值",只能通過一些特定的手段
但是這個方法,主線程和 t線程的耦合太大了
而Callable就是為了會更優(yōu)雅的解決上面的問題
但是Thread并沒有提供這樣的構造方法
我們可以將callable傳入FutureTask
3.2 RentrantLock
表示可重入的鎖
相對于我們常用的Synchronized,ReentrantLock是"手動"進行加鎖和解鎖的
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//加鎖
lock.lock();
//解鎖
lock.unlock();
}
但是這種就容易"漏掉"解鎖操作,就會出現(xiàn)大問題,因此我們經常搭配finally使用
既然這個這么麻煩,那還有存在的價值嘛??
實際上價值還是很大的
ReentrantLock提供了公平鎖的實現(xiàn)
如果傳入true就是表示公平鎖,傳入false / 不傳 就是非公平鎖
ReentrantLock提供了tryLock
所謂tryLock就是嘗試加鎖
如果在遇到鎖已經被占有了,那就直接返回
而相比于synchronized則是阻塞等待
另外,除了直接返回外,tryLock還提供了帶等待超時的版本
Condition
Synchronized是搭配 wait 和 notify使用
而ReentrantLock是搭配Condition使用
實際上Condition比wait和notify更加智能,因為它可以指定喚醒那個線程
3.3 Semaphore
表示信號量,用來表示"可用資源"的個數(shù),本質上就是個計數(shù)器
圍繞信號量主要有兩個基本操作
(1)P操作,即申請資源,計數(shù)器 -1;
(2)V操作,即釋放資源,計數(shù)器+1;
但我們申請的資源超過信號量本身的大小們,就會阻塞等待,直到其他地方釋放資源
那么當資源數(shù)目為1的話,就可以當成鎖來使用了
因為如果信號量有0 1兩個取值,此時就是"二元信號量",本質上就是一把鎖 |
3.4CountDownLatch
表示同時等待多個線程結束
是一個比較實用的工具
當我們把一個任務拆解成多個線程來完成時,就可以利用這個工具類來判斷,任務整體是否完成了
此時的執(zhí)行結果就是:
await會阻塞等待,一直到countDown調用的次數(shù),和構造方法指定的次數(shù)一致的時候,await才會返回
而await不僅僅能夠替代join,假設現(xiàn)在有1000個任務要交給4個線程來使用,那么如何判斷1000個任務已經執(zhí)行結束??就可以使用countDownLatch來判斷 |
4.線程安全的集合類
原來的集合類.比如ArrayList,LinkedList,HashMap等等,都是線程不安全的
而Vector自帶了synchronized,Stack繼承了ector,HashTable也是自帶的synchronized,在一定程度上是線程安全的
但是不能說太絕對,還是要具體情況具體分析 就比如可能出現(xiàn)下面這種情況:
就比如上述代碼,線程1執(zhí)行到if條件判斷后,線程2把vector給清空了,就會出現(xiàn)bug
如果需要用到其他的類,就需要手動加鎖,來保證線程安全,但不同情況下加鎖的情況是不一樣的,手動加鎖是比較麻煩的
標準庫就提供了一些具體的解決方法
4.1多線程環(huán)境下使用ArrayList
4.1.1Collection.synchronizedList(new ArrayList)
這種方法就相當于給這些集合類套了一層殼,殼上對集合類里面的一些關鍵方法加上了鎖,起到了類似Vector的效果
4.1.2CopyOnWriteArrayList
利用的是"寫時拷貝"的思想
假設我們現(xiàn)在有一組數(shù)據(jù)為1 2 3 4,此時某個線程對數(shù)據(jù)進行了修改,就把2 修改成200,3修改成300,但是在修改的時候有別到線程在讀,如果直接修改就有可能出現(xiàn)2,300這樣的中間數(shù)據(jù)
而寫時拷貝就是將原來的數(shù)據(jù)集拷貝一份,這樣修改的時候是在新拷貝的數(shù)據(jù)集上修改的,而讀的時候是在舊的數(shù)據(jù)集上讀的
等到修改完后,就用新的數(shù)據(jù)集的引用代替原來舊的數(shù)據(jù)集的引用
這樣的過程中,不會出現(xiàn)任何加鎖和阻塞等待,也保證讀數(shù)據(jù)不會出現(xiàn)"錯誤的數(shù)據(jù)"
這種操作實際上實用性非常高,就比如有的服務器需要更新配置文件 / 數(shù)據(jù)文件,就可以采取上述策略 |
4.2多線程使用隊列
直接使用BlockingQueue即可
4.3多線程使用哈希表
HashMap是線程不安全的,而HashTable是帶鎖的
但是實際上HashTable并不推薦使用
因為HashTable本質上就是簡單粗暴將每一個方法都進行加鎖,就相當于針對了this加鎖,此時只要針對HashTable上的元素進行操作,就都會涉及到鎖
推薦使用的是 ConcurrentHashTable
它的優(yōu)點就在于:
(1)采用鎖桶的方式,來代替之前的"全局一把鎖"
此時如果兩個線程針對的是不同鏈表上的元素進行操作,是不會涉及到鎖沖突的
而本身,操作兩個鏈表上的元素,不涉及公共變量,是不會有線程安全問題的
進行這樣的操作實際上收益是很多的
因為在一個Hash表里面,桶的數(shù)量是很多的,此時按照我們上面的操作進行加鎖,大部分情況是可以避免鎖沖突的
那么好像鎖多了,鎖對象就多了是不是更加麻煩了??
實際上,由于java中任何的對象都可以作為鎖對象,我們只需將每一個鏈表的頭結點作為鎖對象即可
(2)引入CAS機制
實際上即使是上面的操作,也不能保證線程安全
像哈希表的size,即使你插入的是不同鏈表的元素,修改的時候也會涉及到多線程修改同一個變量
此時引入了CAS機制,通過CAS來修改size,也就不涉及加鎖操作了
(3)針對擴容進行了特殊優(yōu)化
在哈希表中,如果發(fā)現(xiàn)負載因子太大了,就需要擴容,而擴容是一比較低效的操作,普通的hash表如果要在一次put完成整個擴容操作,就會使得put非常卡,如果平時使用put假設是1ms,但某次put執(zhí)行了1000ms,就會造成不好的體驗
ConcurrentHashMap進行的實際上是"化整為零",在擴容的時候會搞兩份空間
一份是擴容前的空間,一份是擴容后的空間
接下載每次進行哈希表的基本操作的時候,都會將一部分數(shù)據(jù)從舊空間搬到新空間
不是一口氣搬完,分多次搬
搬的過程中,
如果進行的是插入操作,那就插到新的空間里面
如果是刪除,那么舊的新的都會刪除
如果是查找,那么舊的新的都要查找一遍
就是"重哈希"過程,重哈希過程結束的標志通常是所有元素都被成功地移動到了新的空間中,并且舊空間中不再包含任何元素。文章來源:http://www.zghlxwxcb.cn/news/detail-857309.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-857309.html
到了這里,關于javaEE初階——多線程(九)——JUC常見的類以及線程安全的集合類的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!