來(lái)源| OSCHINA 社區(qū)
作者 | 京東云開發(fā)者-京東健康 張娜
一、并發(fā)編程的意義與挑戰(zhàn)
并發(fā)編程的意義是充分的利用處理器的每一個(gè)核,以達(dá)到最高的處理性能,可以讓程序運(yùn)行的更快。而處理器也為了提高計(jì)算速率,作出了一系列優(yōu)化,比如:
1、硬件升級(jí):為平衡 CPU 內(nèi)高速存儲(chǔ)器和內(nèi)存之間數(shù)量級(jí)的速率差,提升整體性能,引入了多級(jí)高速緩存的傳統(tǒng)硬件內(nèi)存架構(gòu)來(lái)解決,帶來(lái)的問(wèn)題是,數(shù)據(jù)同時(shí)存在于高速緩存和主內(nèi)存中,需要解決緩存一致性問(wèn)題。
2、處理器優(yōu)化:主要包含,編譯器重排序、指令級(jí)重排序、內(nèi)存系統(tǒng)重排序。通過(guò)單線程語(yǔ)義、指令級(jí)并行重疊執(zhí)行、緩存區(qū)加載存儲(chǔ) 3 種級(jí)別的重排序,減少執(zhí)行指令,從而提高整體運(yùn)行速度。帶來(lái)的問(wèn)題是,多線程環(huán)境里,編譯器和 CPU 指令無(wú)法識(shí)別多個(gè)線程之間存在的數(shù)據(jù)依賴性,影響程序執(zhí)行結(jié)果。
并發(fā)編程的好處是巨大的,然而要編寫一個(gè)線程安全并且執(zhí)行高效的代碼,需要管理可變共享狀態(tài)的操作訪問(wèn),考慮內(nèi)存一致性、處理器優(yōu)化、指令重排序問(wèn)題。比如我們使用多線程對(duì)同一個(gè)對(duì)象的值進(jìn)行操作時(shí)會(huì)出現(xiàn)值被更改、值不同步的情況,得到的結(jié)果和理論值可能會(huì)天差地別,此時(shí)該對(duì)象就不是線程安全的。而當(dāng)多個(gè)線程訪問(wèn)某個(gè)數(shù)據(jù)時(shí),不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程如何交替執(zhí)行,這個(gè)計(jì)算邏輯始終都表現(xiàn)出正確的行為,那么稱這個(gè)對(duì)象是線程安全的。因此如何在并發(fā)編程中保證線程安全是一個(gè)容易忽略的問(wèn)題,也是一個(gè)不小的挑戰(zhàn)。
所以,為什么會(huì)有線程安全的問(wèn)題,首先要明白兩個(gè)關(guān)鍵問(wèn)題:
1、線程之間是如何通信的,即線程之間以何種機(jī)制來(lái)交換信息。
2、線程之間是如何同步的,即程序如何控制不同線程間的發(fā)生順序。
二、Java 并發(fā)編程
Java 并發(fā)采用了共享內(nèi)存模型,Java 線程之間的通信總是隱式進(jìn)行的,整個(gè)通信過(guò)程對(duì)程序員完全透明。
2.1 Java 內(nèi)存模型
為了平衡程序員對(duì)內(nèi)存可見性盡可能高(對(duì)編譯器和處理的約束就多)和提高計(jì)算性能(盡可能少約束編譯器處理器)之間的關(guān)系,JAVA 定義了Java 內(nèi)存模型(Java Memory Model,JMM),約定只要不改變程序執(zhí)行結(jié)果,編譯器和處理器怎么優(yōu)化都行。所以,JMM 主要解決的問(wèn)題是,通過(guò)制定線程間通信規(guī)范,提供內(nèi)存可見性保證。
JMM 結(jié)構(gòu)如下圖所示:
以此看來(lái),線程內(nèi)創(chuàng)建的局部變量、方法定義參數(shù)等只在線程內(nèi)使用不會(huì)有并發(fā)問(wèn)題,對(duì)于共享變量,JMM 規(guī)定了一個(gè)線程如何和何時(shí)可以看到由其他線程修改過(guò)后的共享變量的值,以及在必須時(shí)如何同步的訪問(wèn)共享變量。
為控制工作內(nèi)存和主內(nèi)存的交互,定義了以下規(guī)范:
?所有的變量都存儲(chǔ)在主內(nèi)存 (Main Memory) 中。
?每個(gè)線程都有一個(gè)私有的本地內(nèi)存 (Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀 / 寫共享變量的拷貝副本。
?線程對(duì)變量的所有操作都必須在本地內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。
?不同的線程之間無(wú)法直接訪問(wèn)對(duì)方本地內(nèi)存中的變量。
具體實(shí)現(xiàn)上定義了八種操作:
1.lock:作用于主內(nèi)存,把變量標(biāo)識(shí)為線程獨(dú)占狀態(tài)。
2.unlock:作用于主內(nèi)存,解除獨(dú)占狀態(tài)。
3.read:作用主內(nèi)存,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存。
4.load:作用于工作內(nèi)存,把 read 操作傳過(guò)來(lái)的變量值放入工作內(nèi)存的變量副本中。
5.use:作用工作內(nèi)存,把工作內(nèi)存當(dāng)中的一個(gè)變量值傳給執(zhí)行引擎。
6.assign:作用工作內(nèi)存,把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量。
7.store:作用于工作內(nèi)存的變量,把工作內(nèi)存的一個(gè)變量的值傳送到主內(nèi)存中。
8.write:作用于主內(nèi)存的變量,把 store 操作傳來(lái)的變量的值放入主內(nèi)存的變量中。
這些操作都滿足以下原則:
?不允許 read 和 load、store 和 write 操作之一單獨(dú)出現(xiàn)。
?對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行 store 和 write 操作)。
2.2 Java 中的并發(fā)關(guān)鍵字
Java 基于以上規(guī)則提供了 volatile、synchronized 等關(guān)鍵字來(lái)保證線程安全,基本原理是從限制處理器優(yōu)化和使用內(nèi)存屏障兩方面解決并發(fā)問(wèn)題。如果是變量級(jí)別,使用 volatile 聲明任何類型變量,同基本數(shù)據(jù)類型變量、引用類型變量一樣具備原子性;如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證,需要使用同步塊技術(shù)。Java 內(nèi)存模型提供了 lock 和 unlock 操作來(lái)滿足這種需求。虛擬機(jī)提供了字節(jié)碼指令 monitorenter 和 monitorexist 來(lái)隱式地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到 Java 代碼中就是同步塊 - synchronized 關(guān)鍵字。
這兩個(gè)字的作用:volatile 僅保證對(duì)單個(gè) volatile 變量的讀 / 寫具有原子性,而鎖的互斥執(zhí)行的特性可以確保整個(gè)臨界區(qū)代碼的執(zhí)行具有原子性。在功能上,鎖比 volatile 更強(qiáng)大,在可伸縮性和執(zhí)行性能上,volatile 更有優(yōu)勢(shì)。
2.3 Java 中的并發(fā)容器與工具類
2.3.1 CopyOnWriteArrayList
CopyOnWriteArrayList 在操作元素時(shí)會(huì)加可重入鎖,一次來(lái)保證寫操作是線程安全的,但是每次添加刪除元素就需要復(fù)制一份新數(shù)組,對(duì)空間有較大的浪費(fèi)。
publicEget(int index){ returnget(getArray(), index); } publicbooleanadd(E e){ finalReentrantLock lock =this.lock; lock.lock(); try{ Object[] elements =getArray(); int len = elements.length; Object[] newElements =Arrays.copyOf(elements, len +1); newElements[len]= e; setArray(newElements); returntrue; }finally{ lock.unlock(); } }
2.3.2 Collections.synchronizedList(new ArrayList<>());
這種方式是在 List 的操作外包加了一層 synchronize 同步控制。需要注意的是在遍歷 List 是還得再手動(dòng)做整體的同步控制。
publicvoidadd(int index,E element){ // SynchronizedList 就是在 List的操作外包加了一層synchronize同步控制 synchronized(mutex){list.add(index, element);} } publicEremove(int index){ synchronized(mutex){return list.remove(index);} }
2.3.3 ConcurrentLinkedQueue
通過(guò)循環(huán) CAS 操作非阻塞的給隊(duì)列添加節(jié)點(diǎn),
publicbooleanoffer(E e){ checkNotNull(e); finalNode newNode =newNode(e); for(Node t = tail, p = t;;){ Node q = p.next; if(q ==null){ // p是尾節(jié)點(diǎn),CAS 將p的next指向newNode. if(p.casNext(null, newNode)){ if(p != t) //tail指向真正尾節(jié)點(diǎn) casTail(t, newNode); returntrue; } } elseif(p == q) // 說(shuō)明p節(jié)點(diǎn)和p的next節(jié)點(diǎn)都等于空,表示這個(gè)隊(duì)列剛初始化,正準(zhǔn)備添加節(jié)點(diǎn),所以返回head節(jié)點(diǎn) p =(t !=(t = tail))? t : head; else // 向后查找尾節(jié)點(diǎn) p =(p != t && t !=(t = tail))? t : q; } }
三、線上案例
3.1 問(wèn)題發(fā)現(xiàn)
在互聯(lián)網(wǎng)醫(yī)院醫(yī)生端,醫(yī)生打開問(wèn)診 IM 聊天頁(yè),需要加載幾十個(gè)功能按鈕。在 2022 年 12 月抗疫期間,QPS 全天都很高,高峰時(shí)是平日的 12 倍,偶現(xiàn)報(bào)警提示按鈕顯示不全,問(wèn)題出現(xiàn)概率大概在百萬(wàn)分之一。
3.2 排查問(wèn)題的詳細(xì)過(guò)程
醫(yī)生問(wèn)診 IM 頁(yè)面的加載屬于業(yè)務(wù)黃金流程,上面的每一個(gè)按鈕就是一個(gè)業(yè)務(wù)線的入口,所以處在核心邏輯的上的報(bào)警均使用自定義報(bào)警,該類報(bào)警不設(shè)置收斂,無(wú)論何種異常包括按鈕個(gè)數(shù)異常就會(huì)立即報(bào)警。
1. 根據(jù)報(bào)警信息,開始排查,卻發(fā)現(xiàn)以下問(wèn)題:
(1)沒(méi)有異常日志:順著異常日志的 logId 排查,過(guò)程中竟然沒(méi)有異常日志,按鈕莫名其妙的變少了。
(2)不能復(fù)現(xiàn):在預(yù)發(fā)環(huán)境,使用相同入?yún)ⅲ?a target="_blank">接口正常返回,無(wú)法復(fù)現(xiàn)。
2. 代碼分析,縮小異常范圍:
醫(yī)生問(wèn)診 IM 按鈕處理分組進(jìn)行:
// 多個(gè)線程結(jié)果集合 List multiButtonList =newArrayList<>();
// 多線程并行處理 Future3. 增加日志線上觀察
由于并發(fā)場(chǎng)景容易引發(fā)子線程失敗的情況,對(duì)各子線程分支增加必要節(jié)點(diǎn)日志上線后觀察:
(1)發(fā)生異常的請(qǐng)求處理過(guò)程中,所有子線程正常處理完成
(2)按鈕缺少個(gè)數(shù)隨機(jī)等于子線程中處理的按鈕個(gè)數(shù)
(3)初步判斷是 ArrayList 并發(fā) addAll 操作異常
4. 模擬復(fù)現(xiàn)
使用 ArrayList 源碼模擬復(fù)現(xiàn)問(wèn)題:
(1)ArrayList 源碼分析:
publicbooleanaddAll(Collection c){ Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew);// Increments modCount //以當(dāng)前size為起點(diǎn),向數(shù)組中追加本次新增對(duì)象 System.arraycopy(a,0, elementData, size, numNew); //更新全局變量size的值,和上一步是非原子操作,引發(fā)并發(fā)問(wèn)題的根源 size += numNew; return numNew !=0; } privatevoidensureCapacityInternal(int minCapacity){ if(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){ minCapacity =Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } privatevoidensureExplicitCapacity(int minCapacity){ modCount++; // overflow-conscious code if(minCapacity - elementData.length >0) grow(minCapacity); } privatevoidgrow(int minCapacity){ // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity +(oldCapacity >>1); if(newCapacity - minCapacity <0) newCapacity = minCapacity; if(newCapacity - MAX_ARRAY_SIZE >0) newCapacity =hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData =Arrays.copyOf(elementData, newCapacity); }(2) 理論分析在 ArrayList 的 add 操作中,變更 size 和增加數(shù)據(jù)操作,不是原子操作。
(3)問(wèn)題復(fù)現(xiàn)復(fù)制源碼創(chuàng)建自定義類,為方便復(fù)現(xiàn)并發(fā)問(wèn)題,增加停頓
publicbooleanaddAll(Collection c){ Object[] a = c.toArray(); int numNew = a.length; //第1次停頓,獲取當(dāng)前size try{ Thread.sleep(1000*timeout1); }catch(InterruptedException e){ e.printStackTrace(); } ensureCapacityInternal(size + numNew);// Increments modCount //第2次停頓,等待copy try{ Thread.sleep(1000*timeout2); }catch(InterruptedException e){ e.printStackTrace(); } System.arraycopy(a,0, elementData, size, numNew); //第3次停頓,等待size+= try{ Thread.sleep(1000*timeout3); }catch(InterruptedException e){ e.printStackTrace(); } size += numNew; return numNew !=0; }
3.3 解決問(wèn)題
使用線程安全工具 Collections.synchronizedList 創(chuàng)建 ArrayList :
List上線觀察后正常。multiButtonList =Collections.synchronizedList(newArrayList<>());
3.4 總結(jié)反思
使用多線程處理問(wèn)題已經(jīng)變得很普遍,但是對(duì)于多線程共同操作的對(duì)象必須使用線程安全的類。
另外,還要搞清楚幾個(gè)靈魂問(wèn)題:
(1)JMM 的靈魂:Happens-before 原則
(2)并發(fā)工具類的靈魂:volatile 變量的讀 / 寫 和 CAS
審核編輯:湯梓紅
-
處理器
+關(guān)注
關(guān)注
68文章
19273瀏覽量
229712 -
cpu
+關(guān)注
關(guān)注
68文章
10858瀏覽量
211641 -
編程
+關(guān)注
關(guān)注
88文章
3614瀏覽量
93704 -
編譯器
+關(guān)注
關(guān)注
1文章
1629瀏覽量
49115 -
線程安全
+關(guān)注
關(guān)注
0文章
13瀏覽量
2458
原文標(biāo)題:關(guān)于并發(fā)編程與線程安全的思考與實(shí)踐
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論