主頁 > 後端開發 > 關于并發編程與執行緒安全的思考與實踐

關于并發編程與執行緒安全的思考與實踐

2023-05-10 07:21:07 後端開發

作者:京東健康 張娜

一、并發編程的意義與挑戰

并發編程的意義是充分的利用處理器的每一個核,以達到最高的處理性能,可以讓程式運行的更快,而處理器也為了提高計算速率,作出了一系列優化,比如:

1、硬體升級:為平衡CPU 內高速存盤器和記憶體之間數量級的速率差,提升整體性能,引入了多級高速快取的傳統硬體記憶體架構來解決,帶來的問題是,資料同時存在于高速快取和主記憶體中,需要解決快取一致性問題,

2、處理器優化:主要包含,編譯器重排序、指令級重排序、記憶體系統重排序,通過單執行緒語意、指令級并行重疊執行、快取區加載存盤3種級別的重排序,減少執行指令,從而提高整體運行速度,帶來的問題是,多執行緒環境里,編譯器和CPU指令無法識別多個執行緒之間存在的資料依賴性,影響程式執行結果,

并發編程的好處是巨大的,然而要撰寫一個執行緒安全并且執行高效的代碼,需要管理可變共享狀態的操作訪問,考慮記憶體一致性、處理器優化、指令重排序問題,比如我們使用多執行緒對同一個物件的值進行操作時會出現值被更改、值不同步的情況,得到的結果和理論值可能會天差地別,此時該物件就不是執行緒安全的,而當多個執行緒訪問某個資料時,不管運行時環境采用何種調度方式或者這些執行緒如何交替執行,這個計算邏輯始終都表現出正確的行為,那么稱這個物件是執行緒安全的,因此如何在并發編程中保證執行緒安全是一個容易忽略的問題,也是一個不小的挑戰,

所以,為什么會有執行緒安全的問題,首先要明白兩個關鍵問題:

1、執行緒之間是如何通信的,即執行緒之間以何種機制來交換資訊,

2、執行緒之間是如何同步的,即程式如何控制不同執行緒間的發生順序,

二、Java并發編程

Java并發采用了共享記憶體模型,Java執行緒之間的通信總是隱式進行的,整個通信程序對程式員完全透明,

2.1 Java記憶體模型

為了平衡程式員對記憶體可見性盡可能高(對編譯器和處理的約束就多)和提高計算性能(盡可能少約束編譯器處理器)之間的關系,JAVA定義了Java記憶體模型(Java Memory Model,JMM),約定只要不改變程式執行結果,編譯器和處理器怎么優化都行,所以,JMM主要解決的問題是,通過制定執行緒間通信規范,提供記憶體可見性保證,

JMM結構如下圖所示:

以此看來,執行緒內創建的區域變數、方法定義引數等只在執行緒內使用不會有并發問題,對于共享變數,JMM規定了一個執行緒如何和何時可以看到由其他執行緒修改過后的共享變數的值,以及在必須時如何同步的訪問共享變數,

為控制作業記憶體和主記憶體的互動,定義了以下規范:

?所有的變數都存盤在主記憶體(Main Memory)中,

?每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中存盤了該執行緒以讀/寫共享變數的拷貝副本,

?執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體,

?不同的執行緒之間無法直接訪問對方本地記憶體中的變數,

具體實作上定義了八種操作:

1.lock:作用于主記憶體,把變數標識為執行緒獨占狀態,

2.unlock:作用于主記憶體,解除獨占狀態,

3.read:作用主記憶體,把一個變數的值從主記憶體傳輸到執行緒的作業記憶體,

4.load:作用于作業記憶體,把read操作傳過來的變數值放入作業記憶體的變數副本中,

5.use:作用作業記憶體,把作業記憶體當中的一個變數值傳給執行引擎,

6.assign:作用作業記憶體,把一個從執行引擎接收到的值賦值給作業記憶體的變數,

7.store:作用于作業記憶體的變數,把作業記憶體的一個變數的值傳送到主記憶體中,

8.write:作用于主記憶體的變數,把store操作傳來的變數的值放入主記憶體的變數中,

這些操作都滿足以下原則:

?不允許read和load、store和write操作之一單獨出現,

?對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作),

2.2 Java中的并發關鍵字

Java基于以上規則提供了volatile、synchronized等關鍵字來保證執行緒安全,基本原理是從限制處理器優化和使用記憶體屏障兩方面解決并發問題,如果是變數級別,使用volatile宣告任何型別變數,同基本資料型別變數、參考型別變數一樣具備原子性;如果應用場景需要一個更大范圍的原子性保證,需要使用同步塊技術,Java記憶體模型提供了lock和unlock操作來滿足這種需求,虛擬機提供了位元組碼指令monitorenter和monitorexist來隱式地使用這兩個操作,這兩個位元組碼指令反映到Java代碼中就是同步塊-synchronized關鍵字,

這兩個字的作用:volatile僅保證對單個volatile變數的讀/寫具有原子性,而鎖的互斥執行的特性可以確保整個臨界區代碼的執行具有原子性,在功能上,鎖比volatile更強大,在可伸縮性和執行性能上,volatile更有優勢,

2.3 Java中的并發容器與工具類

2.3.1 CopyOnWriteArrayList

CopyOnWriteArrayList在操作元素時會加可重入鎖,一次來保證寫操作是執行緒安全的,但是每次添加洗掉元素就需要復制一份新陣列,對空間有較大的浪費,

    public E get(int index) {
        return get(getArray(), index);
    }

    public boolean add(E e) {
        final ReentrantLock 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);
            return true;
        } finally {
            lock.unlock();
        }
    }

2.3.2 Collections.synchronizedList(new ArrayList<>());

這種方式是在 List的操作外包加了一層synchronize同步控制,需要注意的是在遍歷List是還得再手動做整體的同步控制,

    public void add(int index, E element) {
        // SynchronizedList 就是在 List的操作外包加了一層synchronize同步控制
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

2.3.3 ConcurrentLinkedQueue

通過回圈CAS操作非阻塞的給佇列添加節點,

    public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p是尾節點,CAS 將p的next指向newNode.
                if (p.casNext(null, newNode)) {
                    if (p != t) 
                        //tail指向真正尾節點
                        casTail(t, newNode);
                    return true;
                }
            }
            else if (p == q)
                // 說明p節點和p的next節點都等于空,表示這個佇列剛初始化,正準備添加節點,所以回傳head節點
                p = (t != (t = tail)) ? t : head;
            else
                // 向后查找尾節點
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

三、線上案例

3.1 問題發現

在互聯網醫院醫生端,醫生打開問診IM聊天頁,需要加載幾十個功能按鈕,在2022年12月抗疫期間,QPS全天都很高,高峰時是平日的12倍,偶現報警提示按鈕顯示不全,問題出現概率大概在百萬分之一,

3.2 排查問題的詳細程序

醫生問診IM頁面的加載屬于業務黃金流程,上面的每一個按鈕就是一個業務線的入口,所以處在核心邏輯的上的報警均使用自定義報警,該類報警不設定收斂,無論何種例外包括按鈕個數例外就會立即報警,

1. 根據報警資訊,開始排查,卻發現以下問題:

(1)沒有例外日志:順著例外日志的logId排查,程序中竟然沒有例外日志,按鈕莫名其妙的變少了,

(2)不能復現:在預發環境,使用相同入參,介面正常回傳,無法復現,

2. 代碼分析,縮小例外范圍:

醫生問診IM按鈕處理分組進行:

    // 多個執行緒結果集合
    List<DoctorDiagImButtonInfoDTO> multiButtonList = new ArrayList<>();
    // 多執行緒并行處理
    Future<List<DoctorDiagImButtonInfoDTO>> multiButtonFuture = joyThreadPoolTaskExecutor.submit(() -> {
        List<DoctorDiagImButtonInfoDTO> multiButtonListTemp = new ArrayList<>();
        buttonTypes.forEach(buttonType -> {
            multiButtonListTemp.add(appButtonInfoMap.get(buttonType));
        });
        multiButtonList.addAll(multiButtonListTemp);
        return multiButtonListTemp;
    });

3. 增加日志線上觀察

由于并發場景容易引發子執行緒失敗的情況,對各子執行緒分支增加必要節點日志上線后觀察:

(1)發生例外的請求處理程序中,所有子執行緒正常處理完成

(2)按鈕缺少個數隨機等于子執行緒中處理的按鈕個數

(3)初步判斷是ArrayList并發addAll操作例外

4. 模擬復現

使用ArrayList原始碼模擬復現問題:

(1)ArrayList原始碼分析:


     public boolean addAll(Collection<? extends E> c) {
         Object[] a = c.toArray();
         int numNew = a.length;
         ensureCapacityInternal(size + numNew); // Increments modCount
 
         //以當前size為起點,向陣列中追加本次新增物件
         System.arraycopy(a, 0, elementData, size, numNew);
 
         //更新全域變數size的值,和上一步是非原子操作,引發并發問題的根源
         size += numNew;
         return numNew != 0;
     }
 
     private void ensureCapacityInternal(int minCapacity) {
         if (elementData =https://www.cnblogs.com/jingdongkeji/archive/2023/05/09/= DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
         }
 
         ensureExplicitCapacity(minCapacity);
     }
 
     private void ensureExplicitCapacity(int minCapacity) {
         modCount++;
 
         // overflow-conscious code
         if (minCapacity - elementData.length > 0)
             grow(minCapacity);
     }
 
     private void grow(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和增加資料操作,不是原子操作,

(3)問題復現

復制原始碼創建自定義類,為方便復現并發問題,增加停頓

     public boolean addAll(Collection<? extends E> c) {
         Object[] a = c.toArray();
         int numNew = a.length;
         //第1次停頓,獲取當前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 解決問題

使用執行緒安全工具 Collections.synchronizedList 創建 ArrayList :

    List<DoctorDiagImButtonInfoDTO> multiButtonList = Collections.synchronizedList(new ArrayList<>()); 

上線觀察后正常,

3.4 總結反思

使用多執行緒處理問題已經變得很普遍,但是對于多執行緒共同操作的物件必須使用執行緒安全的類,

另外,還要搞清楚幾個靈魂問題:

(1)JMM的靈魂:Happens-before 原則

(2)并發工具類的靈魂:volatile變數的讀/寫 和 CAS

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551986.html

標籤:其他

上一篇:由淺入深學MySQL之事務全攻略

下一篇:返回列表

標籤雲
其他(158694) Python(38124) JavaScript(25407) Java(18024) C(15222) 區塊鏈(8262) C#(7972) AI(7469) 爪哇(7425) MySQL(7172) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5336) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4570) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2432) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1965) Web開發(1951) HtmlCss(1932) python-3.x(1918) 弹簧靴(1913) C++(1912) xml(1889) PostgreSQL(1875) .NETCore(1857) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 關于并發編程與執行緒安全的思考與實踐

    并發編程的意義是充分的利用處理器的每一個核,以達到最高的處理性能,可以讓程式運行的更快。而處理器也為了提高計算速率,作出了一系列優化 ......

    uj5u.com 2023-05-10 07:21:07 more
  • 由淺入深學MySQL之事務全攻略

    前言 從今天開始本系列就帶各位小伙伴學習資料庫技術。資料庫技術是Java開發中必不可少的一部分知識內容。也是非常重要的技術。本系列教程由淺入深, 全面講解資料庫體系。 非常適合零基礎的小伙伴來學習。 全文大約 【1707】 字,不說廢話,只講可以讓你學到技術、明白原理的純干貨!本文帶有豐富案例及配圖 ......

    uj5u.com 2023-05-10 07:19:57 more
  • Python3.10動態修改Windows系統(win10/win11)本地IP地址(靜態IP)

    一般情況下,局域網里的終端比如本地服務器設定靜態IP的好處是可以有效減少網路連接時間,原因是程序中省略了每次聯網后從DHCP服務器獲取IP地址的流程,缺點是容易引發IP地址的沖突,當然,還有操作層面的繁瑣,如果想要切換靜態IP地址,就得去網路連接設定中手動操作,本次我們使用Python3.10動態地 ......

    uj5u.com 2023-05-09 08:37:46 more
  • Python3.10動態修改Windows系統(win10/win11)本地IP地址(靜態IP)

    一般情況下,局域網里的終端比如本地服務器設定靜態IP的好處是可以有效減少網路連接時間,原因是程序中省略了每次聯網后從DHCP服務器獲取IP地址的流程,缺點是容易引發IP地址的沖突,當然,還有操作層面的繁瑣,如果想要切換靜態IP地址,就得去網路連接設定中手動操作,本次我們使用Python3.10動態地 ......

    uj5u.com 2023-05-09 08:31:53 more
  • Spring 注解

    @SpringBootApplication 申明讓spring boot自動給程式進行必要的配置,這個配置等同于: @Configuration ,@EnableAutoConfiguration 和 @ComponentScan 三個配置。 @RequestMapping 提供路由資訊,負責UR ......

    uj5u.com 2023-05-09 07:48:30 more
  • SpringSecurity:OAuth2 Client 結合GitHub授權案例(特簡單版)

    3)OAuth2 Client 結合GitHub授權案例 本隨筆說明:這僅作為OAuth2 Client初次使用的案例,所以寫得很簡單,有許多的不足之處。 OAuth2 Client(OAuth2客戶端)是指使用OAuth2協議與授權服務器進行通信并獲取訪問令牌的應用程式或服務。OAuth2客戶端代 ......

    uj5u.com 2023-05-09 07:48:16 more
  • 原來Spring能注入集合和Map的computeIfAbsent是這么好用!

    大家好,我是3y,今天繼續來聊我的開源專案austin啊,但實際內容更新不多。這文章主是想吹下水,主要聊聊我在更新專案中學到的小技巧。 今天所說的小技巧可能有很多人都會,但肯定也會有跟我一樣之前沒用過的。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息 ......

    uj5u.com 2023-05-09 07:48:05 more
  • 京東面經總結

    非科班,經歷了無數場秋招,現將面試京東的題目記錄如下: 一面 kafka在應用場景以及 專案 里的實作 bitmap底層 object里有哪些方法 hashmap相關 sychronized和reentrantlock相關問題以及鎖升級 cas和volatile 執行緒幾種狀態以及轉化 jvm記憶體模型 ......

    uj5u.com 2023-05-09 07:48:01 more
  • @RequestParam注解引數

    做業務的時候經常忘記@RequestParam注解引數,記錄一下 首先,我們要清楚@RequestParam是干什么的 @RequestParam:將請求引數系結到你控制器的方法引數上,路徑上有個引數+? @RequestParam注解引數: 語法:@RequestParam(value=https://www.cnblogs.com/zwy-yjy/archive/2023/05/08/”引數名” ......

    uj5u.com 2023-05-09 07:47:57 more
  • Django筆記三十八之發送郵件

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記三十八之發送郵件 這一篇筆記介紹如何在 Django 中發送郵件。 在 Python 中,提供了 smtplib 的郵件模塊,而 Django 在這個基礎上對其進行了封裝,我們可以通過 django.core.mail 來呼叫。 以下是本 ......

    uj5u.com 2023-05-09 07:47:50 more