摘要:而且只要他更新完畢對(duì)修飾的變量賦值,那么讀線程立馬可以看到最新修改后的數(shù)組,這是保證的。這個(gè)時(shí)候,就采用了思想來(lái)實(shí)現(xiàn)這個(gè),避免更新的時(shí)候阻塞住高頻的讀操作,實(shí)現(xiàn)無(wú)鎖的效果,優(yōu)化線程并發(fā)的性能。
“ 今天聊一個(gè)非常硬核的技術(shù)知識(shí),給大家分析一下CopyOnWrite思想是什么,以及在Java并發(fā)包中的具體體現(xiàn),包括在Kafka內(nèi)核源碼中是如何運(yùn)用這個(gè)思想來(lái)優(yōu)化并發(fā)性能的。1、讀多寫(xiě)少的場(chǎng)景下引發(fā)的問(wèn)題?這個(gè)CopyOnWrite在面試的時(shí)候,很可能成為面試官的一個(gè)殺手锏把候選人給一擊必殺,也很有可能成為候選人拿下Offer的獨(dú)門(mén)秘籍,是相對(duì)高級(jí)的一個(gè)知識(shí)。
大家可以設(shè)想一下現(xiàn)在我們的內(nèi)存里有一個(gè)ArrayList,這個(gè)ArrayList默認(rèn)情況下肯定是線程不安全的,要是多個(gè)線程并發(fā)讀和寫(xiě)這個(gè)ArrayList可能會(huì)有問(wèn)題。
好,問(wèn)題來(lái)了,我們應(yīng)該怎么讓這個(gè)ArrayList變成線程安全的呢?
有一個(gè)非常簡(jiǎn)單的辦法,對(duì)這個(gè)ArrayList的訪問(wèn)都加上線程同步的控制。
比如說(shuō)一定要在synchronized代碼段來(lái)對(duì)這個(gè)ArrayList進(jìn)行訪問(wèn),這樣的話,就能同一時(shí)間就讓一個(gè)線程來(lái)操作它了,或者是用ReadWriteLock讀寫(xiě)鎖的方式來(lái)控制,都可以。
我們假設(shè)就是用ReadWriteLock讀寫(xiě)鎖的方式來(lái)控制對(duì)這個(gè)ArrayList的訪問(wèn)。
這樣多個(gè)讀請(qǐng)求可以同時(shí)執(zhí)行從ArrayList里讀取數(shù)據(jù),但是讀請(qǐng)求和寫(xiě)請(qǐng)求之間互斥,寫(xiě)請(qǐng)求和寫(xiě)請(qǐng)求也是互斥的。
大家看看,代碼大概就是類似下面這樣:
public Object read() { lock.readLock().lock(); // 對(duì)ArrayList讀取 lock.readLock().unlock(); } public void write() { lock.writeLock().lock(); // 對(duì)ArrayList寫(xiě) lock.writeLock().unlock(); }
大家想想,類似上面的代碼有什么問(wèn)題呢?
最大的問(wèn)題,其實(shí)就在于寫(xiě)鎖和讀鎖的互斥。假設(shè)寫(xiě)操作頻率很低,讀操作頻率很高,是寫(xiě)少讀多的場(chǎng)景。
那么偶爾執(zhí)行一個(gè)寫(xiě)操作的時(shí)候,是不是會(huì)加上寫(xiě)鎖,此時(shí)大量的讀操作過(guò)來(lái)是不是就會(huì)被阻塞住,無(wú)法執(zhí)行?
這個(gè)就是讀寫(xiě)鎖可能遇到的最大的問(wèn)題。
2、引入 CopyOnWrite 思想解決問(wèn)題這個(gè)時(shí)候就要引入CopyOnWrite思想來(lái)解決問(wèn)題了。
他的思想就是,不用加什么讀寫(xiě)鎖,鎖統(tǒng)統(tǒng)給我去掉,有鎖就有問(wèn)題,有鎖就有互斥,有鎖就可能導(dǎo)致性能低下,你阻塞我的請(qǐng)求,導(dǎo)致我的請(qǐng)求都卡著不能執(zhí)行。
那么他怎么保證多線程并發(fā)的安全性呢?
很簡(jiǎn)單,顧名思義,利用“CopyOnWrite”的方式,這個(gè)英語(yǔ)翻譯成中文,大概就是“寫(xiě)數(shù)據(jù)的時(shí)候利用拷貝的副本來(lái)執(zhí)行”。
你在讀數(shù)據(jù)的時(shí)候,其實(shí)不加鎖也沒(méi)關(guān)系,大家左右都是一個(gè)讀罷了,互相沒(méi)影響。
問(wèn)題主要是在寫(xiě)的時(shí)候,寫(xiě)的時(shí)候你既然不能加鎖了,那么就得采用一個(gè)策略。
假如說(shuō)你的ArrayList底層是一個(gè)數(shù)組來(lái)存放你的列表數(shù)據(jù),那么這時(shí)比如你要修改這個(gè)數(shù)組里的數(shù)據(jù),你就必須先拷貝這個(gè)數(shù)組的一個(gè)副本。
然后你可以在這個(gè)數(shù)組的副本里寫(xiě)入你要修改的數(shù)據(jù),但是在這個(gè)過(guò)程中實(shí)際上你都是在操作一個(gè)副本而已。
這樣的話,讀操作是不是可以同時(shí)正常的執(zhí)行?這個(gè)寫(xiě)操作對(duì)讀操作是沒(méi)有任何的影響的吧!
大家看下面的圖,一起來(lái)體會(huì)一下這個(gè)過(guò)程:
關(guān)鍵問(wèn)題來(lái)了,那那個(gè)寫(xiě)線程現(xiàn)在把副本數(shù)組給修改完了,現(xiàn)在怎么才能讓讀線程感知到這個(gè)變化呢?
關(guān)鍵點(diǎn)來(lái)了,劃重點(diǎn)!這里要配合上volatile關(guān)鍵字的使用。
筆者之前寫(xiě)過(guò)文章,給大家解釋過(guò)volatile關(guān)鍵字的使用,核心就是讓一個(gè)變量被寫(xiě)線程給修改之后,立馬讓其他線程可以讀到這個(gè)變量引用的最近的值,這就是volatile最核心的作用。
所以一旦寫(xiě)線程搞定了副本數(shù)組的修改之后,那么就可以用volatile寫(xiě)的方式,把這個(gè)副本數(shù)組賦值給volatile修飾的那個(gè)數(shù)組的引用變量了。
只要一賦值給那個(gè)volatile修飾的變量,立馬就會(huì)對(duì)讀線程可見(jiàn),大家都能看到最新的數(shù)組了。
下面是JDK里的 CopyOnWriteArrayList 的源碼。
大家看看寫(xiě)數(shù)據(jù)的時(shí)候,他是怎么拷貝一個(gè)數(shù)組副本,然后修改副本,接著通過(guò)volatile變量賦值的方式,把修改好的數(shù)組副本給更新回去,立馬讓其他線程可見(jiàn)的。
// 這個(gè)數(shù)組是核心的,因?yàn)橛胿olatile修飾了 // 只要把最新的數(shù)組對(duì)他賦值,其他線程立馬可以看到最新的數(shù)組 private transient volatile Object[] array; public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 對(duì)數(shù)組拷貝一個(gè)副本出來(lái) Object[] newElements = Arrays.copyOf(elements, len + 1); // 對(duì)副本數(shù)組進(jìn)行修改,比如在里面加入一個(gè)元素 newElements[len] = e; // 然后把副本數(shù)組賦值給volatile修飾的變量 setArray(newElements); return true; } finally { lock.unlock(); } }
然后大家想,因?yàn)槭峭ㄟ^(guò)副本來(lái)進(jìn)行更新的,萬(wàn)一要是多個(gè)線程都要同時(shí)更新呢?那搞出來(lái)多個(gè)副本會(huì)不會(huì)有問(wèn)題?
當(dāng)然不能多個(gè)線程同時(shí)更新了,這個(gè)時(shí)候就是看上面源碼里,加入了lock鎖的機(jī)制,也就是同一時(shí)間只有一個(gè)線程可以更新。
那么更新的時(shí)候,會(huì)對(duì)讀操作有任何的影響嗎?
絕對(duì)不會(huì),因?yàn)樽x操作就是非常簡(jiǎn)單的對(duì)那個(gè)數(shù)組進(jìn)行讀而已,不涉及任何的鎖。而且只要他更新完畢對(duì)volatile修飾的變量賦值,那么讀線程立馬可以看到最新修改后的數(shù)組,這是volatile保證的。
這樣就完美解決了我們之前說(shuō)的讀多寫(xiě)少的問(wèn)題。
如果用讀寫(xiě)鎖互斥的話,會(huì)導(dǎo)致寫(xiě)鎖阻塞大量讀操作,影響并發(fā)性能。
但是如果用了CopyOnWriteArrayList,就是用空間換時(shí)間,更新的時(shí)候基于副本更新,避免鎖,然后最后用volatile變量來(lái)賦值保證可見(jiàn)性,更新的時(shí)候?qū)ψx線程沒(méi)有任何的影響!
3、CopyOnWrite 思想在Kafka源碼中的運(yùn)用
在Kafka的內(nèi)核源碼中,有這么一個(gè)場(chǎng)景,客戶端在向Kafka寫(xiě)數(shù)據(jù)的時(shí)候,會(huì)把消息先寫(xiě)入客戶端本地的內(nèi)存緩沖,然后在內(nèi)存緩沖里形成一個(gè)Batch之后再一次性發(fā)送到Kafka服務(wù)器上去,這樣有助于提升吞吐量。
話不多說(shuō),大家看下圖:
這個(gè)時(shí)候Kafka的內(nèi)存緩沖用的是什么數(shù)據(jù)結(jié)構(gòu)呢?大家看源碼:
private final ConcurrentMapbatches = new CopyOnWriteMap ();
這個(gè)數(shù)據(jù)結(jié)構(gòu)就是核心的用來(lái)存放寫(xiě)入內(nèi)存緩沖中的消息的數(shù)據(jù)結(jié)構(gòu),要看懂這個(gè)數(shù)據(jù)結(jié)構(gòu)需要對(duì)很多Kafka內(nèi)核源碼里的概念進(jìn)行解釋,這里先不展開(kāi)。
但是大家關(guān)注一點(diǎn),他是自己實(shí)現(xiàn)了一個(gè)CopyOnWriteMap,這個(gè)CopyOnWriteMap采用的就是CopyOnWrite思想。
我們來(lái)看一下這個(gè)CopyOnWriteMap的源碼實(shí)現(xiàn):
// 典型的volatile修飾普通Map private volatile Mapmap; @Override public synchronized V put(K k, V v) { // 更新的時(shí)候先創(chuàng)建副本,更新副本,然后對(duì)volatile變量賦值寫(xiě)回去 Mapcopy= new HashMap(this.map); V prev = copy.put(k, v); this.map = Collections.unmodifiableMap(copy); return prev; } @Override public V get(Object k) { // 讀取的時(shí)候直接讀volatile變量引用的map數(shù)據(jù)結(jié)構(gòu),無(wú)需鎖 return map.get(k); }
所以Kafka這個(gè)核心數(shù)據(jù)結(jié)構(gòu)在這里之所以采用CopyOnWriteMap思想來(lái)實(shí)現(xiàn),就是因?yàn)檫@個(gè)Map的key-value對(duì),其實(shí)沒(méi)那么頻繁更新。
也就是TopicPartition-Deque這個(gè)key-value對(duì),更新頻率很低。
但是他的get操作卻是高頻的讀取請(qǐng)求,因?yàn)闀?huì)高頻的讀取出來(lái)一個(gè)TopicPartition對(duì)應(yīng)的Deque數(shù)據(jù)結(jié)構(gòu),來(lái)對(duì)這個(gè)隊(duì)列進(jìn)行入隊(duì)出隊(duì)等操作,所以對(duì)于這個(gè)map而言,高頻的是其get操作。
這個(gè)時(shí)候,Kafka就采用了CopyOnWrite思想來(lái)實(shí)現(xiàn)這個(gè)Map,避免更新key-value的時(shí)候阻塞住高頻的讀操作,實(shí)現(xiàn)無(wú)鎖的效果,優(yōu)化線程并發(fā)的性能。
相信大家看完這個(gè)文章,對(duì)于CopyOnWrite思想以及適用場(chǎng)景,包括JDK中的實(shí)現(xiàn),以及在Kafka源碼中的運(yùn)用,都有了一個(gè)切身的體會(huì)了。
如果你能在面試時(shí)說(shuō)清楚這個(gè)思想以及他在JDK中的體現(xiàn),并且還能結(jié)合知名的開(kāi)源項(xiàng)目 Kafka 的底層源碼進(jìn)一步向面試官進(jìn)行闡述,面試官對(duì)你的印象肯定大大的加分。
本文轉(zhuǎn)載自:http://www.imooc.com/article/...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74523.html
摘要:可現(xiàn)在五年過(guò)去了,他想跳槽卻鮮有人問(wèn)津。最可氣的是比他晚一年畢業(yè)的學(xué)弟,勤勤懇懇在一家中型互聯(lián)網(wǎng)企業(yè)干了年,現(xiàn)在已經(jīng)跳槽到了阿里,月薪是我這個(gè)同學(xué)的倍。 我有個(gè)同學(xué)大學(xué)畢業(yè),因?yàn)閰s少工作經(jīng)驗(yàn),又不愿意去正經(jīng)的互聯(lián)網(wǎng)企業(yè)做實(shí)習(xí)生,他嫌工資太低,于是進(jìn)了家外包公司,那時(shí)候感覺(jué)待遇還可以??涩F(xiàn)在五...
摘要:有可能,會(huì)造成優(yōu)先級(jí)反轉(zhuǎn)或者饑餓現(xiàn)象。悲觀鎖在中的使用,就是利用各種鎖。對(duì)于而言,其是獨(dú)享鎖。偏向鎖,顧名思義,它會(huì)偏向于第一個(gè)訪問(wèn)鎖的線程,大多數(shù)情況下鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得。 理解鎖的基礎(chǔ)知識(shí) 如果想要透徹的理解java鎖的來(lái)龍去脈,需要先了解以下基礎(chǔ)知識(shí)。 基礎(chǔ)知識(shí)之一:鎖的類型 按照其性質(zhì)分類 公平鎖/非公平鎖 公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲...
摘要:線程安全的線程安全的,在讀多寫(xiě)少的場(chǎng)合性能非常好,遠(yuǎn)遠(yuǎn)好于高效的并發(fā)隊(duì)列,使用鏈表實(shí)現(xiàn)。這樣帶來(lái)的好處是在高并發(fā)的情況下,你會(huì)需要一個(gè)全局鎖來(lái)保證整個(gè)平衡樹(shù)的線程安全。 該文已加入開(kāi)源項(xiàng)目:JavaGuide(一份涵蓋大部分Java程序員所需要掌握的核心知識(shí)的文檔類項(xiàng)目,Star 數(shù)接近 14 k)。地址:https://github.com/Snailclimb... 一 JDK ...
摘要:還是老規(guī)矩,從易到難吧傳統(tǒng)的定時(shí)器,異步編程等。分配對(duì)象時(shí),先是在空間中進(jìn)行分配。內(nèi)存泄漏內(nèi)存泄漏是指程序中己動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無(wú)法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。 showImg(https://segmentfault.com/img/bVbwkad?w=1286&h=876); 網(wǎng)上參差不棄的面試題,本文由淺入深,讓你在...
閱讀 2287·2021-11-10 11:35
閱讀 899·2021-09-26 09:55
閱讀 2388·2021-09-22 15:22
閱讀 2318·2021-09-22 15:17
閱讀 3683·2021-09-09 09:33
閱讀 1821·2019-08-30 11:22
閱讀 970·2019-08-30 10:57
閱讀 641·2019-08-29 16:10