併發編程之無鎖

来源:https://www.cnblogs.com/haizai/archive/2020/02/16/12318635.html
-Advertisement-
Play Games

併發編程之無鎖 6.2 CAS與volatile 源碼之LongAdder 6.8 Unsafe 6.2 CAS與volatile 其中的關鍵是compareAndSet,它的簡稱就是CAS(也有Compare And Swap的說法),它必須是原子操作。註意其實CAS的底層是lock cmpxch ...


併發編程之無鎖

 

6.2 CAS與volatile

在這裡插入圖片描述
其中的關鍵是compareAndSet,它的簡稱就是CAS(也有Compare And Swap的說法),它必須是原子操作。
在這裡插入圖片描述
註意
其實CAS的底層是lock cmpxchg指令(X86架構),在單核CPU和多核CPU下都能夠保證【比較-交換】的原子性。

  • 在多核狀態下,某個核執行到帶lock的指令時,CPU會讓匯流排鎖住,當這個核把此指令執行完畢,再開啟匯流排。這個過程中不會被線程的調度機制所打斷,保證了多個線程對記憶體操作的準確性,是原子的。
    volatile
    獲取共用變數時,為了保證變數的可見性,需要使用volatile修飾。
    它可以用來修飾成員變數和靜態成員變數,他可以避免線程從自己的工作緩存中查找變數的值,必須到主存中獲取它的值,線程操作volatile變數都是直接操作主存。即一個線程對volatile變數的修改,對另一個線程可見。
    註意 :
    volatile僅僅保證了共用變數的可見性,讓其它線程能夠看到最新值,但不能解決指令交錯問題(不能保證原子性)
    CAS必須藉助volatile才能讀取到共用變數的最新值來實現【比較並交換】的效果
    為什麼無鎖效率高
  • 無鎖情況下,即使重試失敗,線程始終在高速運行,沒有停歇,而synchronized會讓線程在沒有獲得鎖的時候,發生上下文切換,進入阻塞。打個比喻
  • 線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車要減速、熄火,等被喚醒又得重新打火、啟動、加速…恢復到高速運行,代價比較大
  • 但無鎖情況下,因為線程要保存運行,需要額外CPU的支持,CPU在這裡就好比高速跑道,沒有額外的跑道,線程想高速運行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,任然會進入可運行狀態,還是會導致上下文切換。
    CAS的特點
    結合CAS和volatile可以實現無鎖併發,適用於線程數少、多核CPU的場景下。
  • CAS是基於樂觀鎖的思想 :最客觀的估計,不怕別的線程來修改共用變數,就算改了也沒關係,我吃虧點再重試。
  • synchronized是基於悲觀鎖的思想 :最悲觀的估計,提防著其他線程來修改共用變數,我上了鎖你們都別想改,我改完瞭解開鎖,你們才有機會。
  • CAS體現的是無鎖併發、無阻塞併發
    • 因為沒有使用synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一
    • 但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響
package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class Test4 {
    public static void main(String[] args) {
        demo(
                () -> new AtomicLong(0),
                (adder) -> adder.getAndIncrement()
        );
        
        demo(
                () -> new LongAdder(),
                (adder) -> adder.increment()
        );
    }

    /**
     *
     * @param adderSupplier : () -> 結果   提供累加器對象
     * @param action        : (參數) -> void  執行累加操作
     * @param <T>
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        List<Thread> threadList = new ArrayList<>();
        // 4個線程,每人累加50萬
        for (int i = 0; i < 4; i++) {
            threadList.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        threadList.forEach(t -> t.start());
        long end = System.nanoTime();
        System.out.println(adder + " cost : " + (end - start));
    }
}
package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class Test3 {

    public static void main(String[] args) {

    }

    /**
     * 參數1,提供數組,可以是線程不安全數組或線程安全數組
     * 參數2,獲取數組長度的方法
     * 參數3,自增方法,回傳array,index
     * 參數4,列印數組的方法
     *
     * @param arraySupplier : 提供者  無中生有  () -> 結果
     * @param lengthFun    : 函數    一個參數一個結果  (參數) -> 結果 ,BiFunction(參數1,參數2) -> 結果
     * @param putConsumer  : 消費者  (參數1,參數2) -> void
     * @param printConsumer : 消費者  (參數) -> void
     * @param <T>
     */
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer
    ) {
        List<Thread> threadList = new ArrayList<>();
        T array = arraySupplier.get();
        Integer length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            // 每個線程對數組做10000次操作
            threadList.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }
        threadList.forEach(thread -> thread.start());
    }

}

輸出
在這裡插入圖片描述
性能提升的原因很簡單,就是在有競爭時,設置多個累加單元,Thread-0累加 Cel【0】,而Thread-1累加Cell【1】。。。最後將結果彙總。這樣它們在累加時操作的不同的Cell變數,因此減少了CAS重試失敗,從而提高性能。

源碼之LongAdder

LongAdder類有幾個關鍵域
在這裡插入圖片描述

  • 原理之偽共用
    其中Cell即為累加單元
    在這裡插入圖片描述
    緩存與記憶體的速度比較
    在這裡插入圖片描述
    在這裡插入圖片描述
    因為CPU與記憶體的速度差異很大,需要靠預讀數據至緩存來提升效率。
    而緩存以緩存行為單位,每個緩存行對應著一塊記憶體,一般是64byte(8個long)
    緩存的加入會造成數據副本的產生,即同一份數據會緩存在不同核心的緩存行中,CPU要保證數據的一致性,如果某個CPU核心更改了數據其它CPU核心對應的整個緩存行必須失效
    在這裡插入圖片描述
    因為Cell是數組形式,在記憶體中是連續存儲的,一個Cell為24位元組(16位元組的對象頭和8位元組的value),因此緩存行可以存下2個的Cell對象。這樣問題來了 :
  • Core-0要修改Cell【0】
  • Core-1要修改Cell【1】
    無論誰修改成功,都會導致對方Core的緩存行失效,比如Core-0中Cell【0】=6000,Cell【1】=8000 要累加Cell【0】=6001,Cell【1】=8000,這時會讓Core-1的緩存行失效
    @sun.misc.Contended用來解決這個問題,它的原理是在使用此註解的對象或欄位的前後各增加128位元組大小的padding,從而讓CPU將對象預讀至緩存時占用不同的緩存行,這樣,不會造成對方緩存行的失效
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述
    在這裡插入圖片描述
    獲取最終結果通過sun方法
    在這裡插入圖片描述

6.8 Unsafe

概述
Unsafe對象提供了非常底層的,操作記憶體、線程的方法,Unsafe對象不能直接調用,只能通過反射獲得
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 資料庫設置 在上一章節中學習瞭如何創建Django項目,在Django項目中創建web應用,以及如何在Django主程式的URL中引用web應用中的URL。下麵來瞭解如何在Django中使用資料庫。Django中想要使用資料庫, 首先要瞭解mysite/mysite/settings.py中關於數據 ...
  • 需求 maven依賴 列印sql 配置要點: 1. 驅動配置 application.properties 2. psy配置 aop列印持久層執行時間 使用aop實現; 啟用aop註解: 小結 來個效果截圖: 通過本片文章,你可以學會: 1. 給代碼添加aop切麵,增加日誌或者列印出方法執行總耗時; ...
  • 一、問題剖析 看到這個問題,我想吹水兩句再做推薦。一般發出這個疑問都處在初學編程階段,編程語言都是相通的,只要你領悟了一門語言的“任督二脈”,以後你學哪一門語言都會輕易上手。學語言嘛,當你工作一兩年了,你還真會覺得像當初老師說的那樣,語言只是工具罷了。工作期間,可能要你接觸到其它語言,而且要你能快速 ...
  • 生產者是如何生產消息 如何創建生產者 發送消息到Kafka 生產者配置 分區 ...
  • GUI編程 組建 視窗 彈窗 面板 文本框 列表框 按鈕 圖片 監聽事件 滑鼠 鍵盤事件 破解工具 1、簡介 GUI的核心技術:Swing AWT 為什麼不流行? 界面不美觀。 需要jre環境。(沒必要為一個5M的小游戲下載幾百M的jre) 但是學了java的GUI編程,有助於瞭解MVC架構,瞭解監 ...
  • 字元和字元串是最常用的信息 1:char表示字元 字元常量-兩個單引號中間的字元表示字元常量'A' 2:字元串和String 字元串常量-雙引號中間的字元序列"Java" 字元串常量是String型的實例的引用。String s="ABC"與String s=new String("ABC"); 3 ...
  • javaSE學習筆記(16) 網路編程 基本概念 如今,電腦已經成為人們學習、工作、生活必不可少的工具。我們利用電腦可以和親朋好友網上聊天,也可以玩網游、發郵件等等,這些功能實現都離不開電腦網路。電腦網路實現了不同電腦之間的通信,這必須依靠編寫網路程式來實現。下麵,我們將教大家如何編寫網路 ...
  • 通常,我們在寫java程式的時候,似乎很少關註記憶體分配和垃圾回收的問題。因為,這部分工作,JVM已經幫我們自動實現了。 這樣看起來,好像很美好,但是任何事情都有兩面性。雖然JVM會自動的進行垃圾回收,但是,如果遇到有些問題,JVM自己也處理不了呢? 因此,我們需要瞭解一下JVM垃圾回收是怎樣運作的, ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...