新聞中心
今天聊一個非常硬核的技術(shù)知識,給大家分析一下 CopyOnWrite 思想是什么,以及在 Java 并發(fā)包中的具體體現(xiàn),包括在 Kafka 內(nèi)核源碼中是如何運用這個思想來優(yōu)化并發(fā)性能的。

為南雄等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務(wù),及南雄網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為做網(wǎng)站、成都網(wǎng)站建設(shè)、南雄網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
這個 CopyOnWrite 在面試的時候,很可能成為面試官的一個殺手锏把候選人給一擊必殺,也很有可能成為候選人拿下 Offer 的獨門秘籍,是相對高級的一個知識。
讀多寫少的場景下引發(fā)的問題?
大家可以設(shè)想一下現(xiàn)在我們的內(nèi)存里有一個 ArrayList,這個 ArrayList 默認(rèn)情況下肯定是線程不安全的,要是多個線程并發(fā)讀和寫這個 ArrayList 可能會有問題。
好,問題來了,我們應(yīng)該怎么讓這個 ArrayList 變成線程安全的呢?有一個非常簡單的辦法,對這個 ArrayList 的訪問都加上線程同步的控制。
比如說一定要在 Synchronized 代碼段來對這個 ArrayList 進(jìn)行訪問,這樣的話,就能同一時間就讓一個線程來操作它了,或者是用 ReadWriteLock 讀寫鎖的方式來控制,都可以。
我們假設(shè)就是用 ReadWriteLock 讀寫鎖的方式來控制對這個 ArrayList 的訪問。
這樣多個讀請求可以同時執(zhí)行從 ArrayList 里讀取數(shù)據(jù),但是讀請求和寫請求之間互斥,寫請求和寫請求也是互斥的。
大家看看,代碼大概就是類似下面這樣:
- public Object read() {
- lock.readLock().lock();
- // 對ArrayList讀取
- lock.readLock().unlock();
- }
- public void write() {
- lock.writeLock().lock();
- // 對ArrayList寫
- lock.writeLock().unlock();
- }
大家想想,類似上面的代碼有什么問題呢?***的問題,其實就在于寫鎖和讀鎖的互斥。假設(shè)寫操作頻率很低,讀操作頻率很高,是寫少讀多的場景。
那么偶爾執(zhí)行一個寫操作的時候,是不是會加上寫鎖,此時大量的讀操作過來是不是就會被阻塞住,無法執(zhí)行?
這個就是讀寫鎖可能遇到的***的問題。
引入 CopyOnWrite 思想解決問題
這個時候就要引入 CopyOnWrite 思想來解決問題了。
它的思想就是,不用加什么讀寫鎖,鎖統(tǒng)統(tǒng)給我去掉,有鎖就有問題,有鎖就有互斥,有鎖就可能導(dǎo)致性能低下,你阻塞我的請求,導(dǎo)致我的請求都卡著不能執(zhí)行。
那么它怎么保證多線程并發(fā)的安全性呢?很簡單,顧名思義,利用“CopyOnWrite”的方式,這個英語翻譯成中文,大概就是“寫數(shù)據(jù)的時候利用拷貝的副本來執(zhí)行”。
你在讀數(shù)據(jù)的時候,其實不加鎖也沒關(guān)系,大家左右都是一個讀罷了,互相沒影響。
問題主要是在寫的時候,寫的時候你既然不能加鎖了,那么就得采用一個策略。
假如說你的 ArrayList 底層是一個數(shù)組來存放你的列表數(shù)據(jù),那么這時比如你要修改這個數(shù)組里的數(shù)據(jù),你就必須先拷貝這個數(shù)組的一個副本。
然后你可以在這個數(shù)組的副本里寫入你要修改的數(shù)據(jù),但是在這個過程中實際上你都是在操作一個副本而已。
這樣的話,讀操作是不是可以同時正常的執(zhí)行?這個寫操作對讀操作是沒有任何的影響的吧!
大家看下面的圖,一起來體會一下這個過程:
關(guān)鍵問題來了,那那個寫線程現(xiàn)在把副本數(shù)組給修改完了,現(xiàn)在怎么才能讓讀線程感知到這個變化呢?
關(guān)鍵點來了,劃重點!這里要配合上 Volatile 關(guān)鍵字的使用。
筆者之前寫過文章,給大家解釋過 Volatile 關(guān)鍵字的使用,核心就是讓一個變量被寫線程給修改之后,立馬讓其他線程可以讀到這個變量引用的最近的值,這就是 Volatile 最核心的作用。
所以一旦寫線程搞定了副本數(shù)組的修改之后,那么就可以用 Volatile 寫的方式,把這個副本數(shù)組賦值給 Volatile 修飾的那個數(shù)組的引用變量了。
只要一賦值給那個 Volatile 修飾的變量,立馬就會對讀線程可見,大家都能看到***的數(shù)組了。
下面是 JDK 里的 CopyOnWriteArrayList 的源碼:
- // 這個數(shù)組是核心的,因為用volatile修飾了
- // 只要把***的數(shù)組對他賦值,其他線程立馬可以看到***的數(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;
- // 對數(shù)組拷貝一個副本出來
- Object[] newElements = Arrays.copyOf(elements, len + 1);
- // 對副本數(shù)組進(jìn)行修改,比如在里面加入一個元素
- newElements[len] = e;
- // 然后把副本數(shù)組賦值給volatile修飾的變量
- setArray(newElements);
- return true;
- } finally {
- lock.unlock();
- }
- }
大家看看寫數(shù)據(jù)的時候,他是怎么拷貝一個數(shù)組副本,然后修改副本,接著通過 Volatile 變量賦值的方式,把修改好的數(shù)組副本給更新回去,立馬讓其他線程可見的。
然后大家想,因為是通過副本來進(jìn)行更新的,萬一要是多個線程都要同時更新呢?那搞出來多個副本會不會有問題?
當(dāng)然不能多個線程同時更新了,這個時候就是看上面源碼里,加入了 Lock 鎖的機(jī)制,也就是同一時間只有一個線程可以更新。
那么更新的時候,會對讀操作有任何的影響嗎?絕對不會,因為讀操作就是非常簡單的對那個數(shù)組進(jìn)行讀而已,不涉及任何的鎖。
而且只要他更新完畢對 Volatile 修飾的變量賦值,那么讀線程立馬可以看到***修改后的數(shù)組,這是 Volatile 保證的:
- private E get(Object[] a, int index) {
- // 最簡單的對數(shù)組進(jìn)行讀取
- return (E) a[index];
- }
這樣就***解決了我們之前說的讀多寫少的問題。如果用讀寫鎖互斥的話,會導(dǎo)致寫鎖阻塞大量讀操作,影響并發(fā)性能。
但是如果用了 CopyOnWriteArrayList,就是用空間換時間,更新的時候基于副本更新,避免鎖,然后***用 Volatile 變量來賦值保證可見性,更新的時候?qū)ψx線程沒有任何的影響!
CopyOnWrite 思想在 Kafka 源碼中的運用
在 Kafka 的內(nèi)核源碼中,有這么一個場景,客戶端在向 Kafka 寫數(shù)據(jù)的時候,會把消息先寫入客戶端本地的內(nèi)存緩沖,然后在內(nèi)存緩沖里形成一個 Batch 之后再一次性發(fā)送到 Kafka 服務(wù)器上去,這樣有助于提升吞吐量。
話不多說,大家看下圖:
這個時候 Kafka 的內(nèi)存緩沖用的是什么數(shù)據(jù)結(jié)構(gòu)呢?大家看源碼:
- private final ConcurrentMap
> batches = - new CopyOnWriteMap
>();
這個數(shù)據(jù)結(jié)構(gòu)就是核心的用來存放寫入內(nèi)存緩沖中的消息的數(shù)據(jù)結(jié)構(gòu),要看懂這個數(shù)據(jù)結(jié)構(gòu)需要對很多 Kafka 內(nèi)核源碼里的概念進(jìn)行解釋,這里先不展開。
但是大家關(guān)注一點,他是自己實現(xiàn)了一個 CopyOnWriteMap,這個CopyOnWriteMap 采用的就是 CopyOnWrite 思想。
我們來看一下這個 CopyOnWriteMap 的源碼實現(xiàn):
- // 典型的volatile修飾普通Map
- private volatile Map
map; - @Override
- public synchronized V put(K k, V v) {
- // 更新的時候先創(chuàng)建副本,更新副本,然后對volatile變量賦值寫回去
- Map
copy = new HashMap (this.map); - V prev = copy.put(k, v);
- this.map = Collections.unmodifiableMap(copy);
- return prev;
- }
- @Override
- public V get(Object k) {
- // 讀取的時候直接讀volatile變量引用的map數(shù)據(jù)結(jié)構(gòu),無需鎖
- return map.get(k);
- }
Kafka 這個核心數(shù)據(jù)結(jié)構(gòu)在這里之所以采用 CopyOnWriteMap 思想來實現(xiàn),就是因為這個 Map 的 Key-Value 對,其實沒那么頻繁更新。
也就是 TopicPartition-Deque 這個 Key-Value 對,更新頻率很低。
但是它的 Get 操作卻是高頻的讀取請求,因為會高頻的讀取出來一個 TopicPartition 對應(yīng)的 Deque 數(shù)據(jù)結(jié)構(gòu),來對這個隊列進(jìn)行入隊出隊等操作,所以對于這個 Map 而言,高頻的是其 Get 操作。
這個時候,Kafka 就采用了 CopyOnWrite 思想來實現(xiàn)這個 Map,避免更新 Key-Value 的時候阻塞住高頻的讀操作,實現(xiàn)無鎖的效果,優(yōu)化線程并發(fā)的性能。
相信大家看完這個文章,對于 CopyOnWrite 思想以及適用場景,包括 JDK 中的實現(xiàn),以及在 Kafka 源碼中的運用,都有了一個切身的體會了。
如果你能在面試時說清楚這個思想以及他在 JDK 中的體現(xiàn),并且還能結(jié)合知名的開源項目 Kafka 的底層源碼進(jìn)一步向面試官進(jìn)行闡述,面試官對你的印象肯定大大的加分。
中華石杉:十余年 BAT 架構(gòu)經(jīng)驗,一線互聯(lián)網(wǎng)公司技術(shù)總監(jiān)。帶領(lǐng)上百人團(tuán)隊開發(fā)過多個億級流量高并發(fā)系統(tǒng)?,F(xiàn)將多年工作中積累下的研究手稿、經(jīng)驗總結(jié)整理成文,傾囊相授。微信公眾號:石杉的架構(gòu)筆記(ID:shishan100)。
標(biāo)題名稱:不會這些“高級貨”,活該你面試當(dāng)炮灰...
本文地址:http://fisionsoft.com.cn/article/dhpoeej.html


咨詢
建站咨詢
