無鎖編程:採用不可變類減少鎖的使用

来源:http://www.cnblogs.com/Binhua-Liu/archive/2016/06/11/5573444.html
-Advertisement-
Play Games

很多的同學很少使用、或者乾脆不瞭解不可變類(Immutable Class)。直觀上很容易認為Immutable類效率不高,或者難以理解他的使用場景。其實不可變類是非常有用的,可以提高並行編程的效率和優化設計。讓我們跳過一些寬泛的介紹,從一個常見的並行編程場景說起: 假設系統需要實時地處理大量的訂單... ...


 

很多的同學很少使用、或者乾脆不瞭解不可變類(Immutable Class)。直觀上很容易認為Immutable類效率不高,或者難以理解他的使用場景。其實不可變類是非常有用的,可以提高並行編程的效率和優化設計。讓我們跳過一些寬泛的介紹,從一個常見的並行編程場景說起:

 

假設系統需要實時地處理大量的訂單,這些訂單的處理依賴於用戶的配置,例如用戶的會員級別、支付方式等。程式需要通過這些配置的參數來計算訂單的價格。而用戶配置同時被另外一些線程更新。顯然,我們在訂單計算的過程中保持配置的一致性。

 

上面的例子是我虛擬出來的,但是類似的場景非常常見--線程A實時地大量地處理請求;線程B偶爾地修改線程A依賴的配置信息。我們陷入這樣的兩難:

1,為了保持配置的一致性,我們不得不線上程A和線程B上,對配置的讀和寫都加鎖,才能保障配置的一致性。這樣才能保證請求處理過程中,不會出現某些配置項被更新了,而另外一些沒有;或者處理中開始使用的是舊配置,而後又使用新的配置。(聽起來類似於資料庫的臟讀問題)

2,另一方面,線程A明顯比線程B更繁忙,為了偶爾一次的配置更新,為每秒數以萬次的請求處理加鎖,顯然代價太高了。

 

解決方案

解決方案有兩種:

第一種是,採用ReadWriteLock。這是最常見的方式。

對讀操作加讀鎖,對寫操作加寫鎖。如果沒有正在發生的寫操作,讀鎖的代價很低。

 

第二種是,採用不可變對象來保存配置信息,用替換配置對象的方式,而不是修改配置對象的方式,來更新配置信息。讓我們來思考一下這麼做的利弊:

1)對於訂單處理線程A來說,它不再需要加鎖了!因為用於保存配置的對象是不可變對象。我們要麼讀取的是一個舊的配置對象,要麼是一個新的配置對象(新的配置對象覆蓋了舊的配置對象)。不會出現“臟讀”的情況。

2)對於用於更新配置的線程B,它的負擔加重了 -- 更新任何一項配置,都必須重新創建一個新的不可變對象,然後把更新的新的屬性和其他舊屬性賦給新的對象,最後覆蓋舊的對象,被拋棄的舊對象還增加了GC的負擔。而原本,這一切只要一個set操作就能完成。

 

我們如何衡量利弊呢?經常,這是非常划算的,線程A和線程B的工作量可能相差幾個數量級。用線程B壓力的增加(其實不值一提)來換取線程A可以不用鎖,效率應該會有很大提升。

 

代碼及性能測試

讓我們用代碼來測試一下哪個解決方案更好。

 

方案一:採用ReentrantReadWriteLock來加讀寫鎖:

一個普通的配置類,保存了用戶的優惠信息,包括會員優惠和特殊節日優惠,在計算訂單總價的時候用到:

public class AccountConfig {
    private double membershipDiscount;
    private double specialEventDiscount;
    
    public AccountConfig(double membershipDiscount, double specialEventDiscount)
    {
        this.membershipDiscount = membershipDiscount;
        this.specialEventDiscount = specialEventDiscount;
    }

    public double getMembershipDiscount() {
        return membershipDiscount;
    }

    public void setMembershipDiscount(double membershipDiscount) {
        this.membershipDiscount = membershipDiscount;
    }

    public double getSpecialEventDiscount() {
        return specialEventDiscount;
    }

    public void setSpecialEventDiscount(double specialEventDiscount) {
        this.specialEventDiscount = specialEventDiscount;
    }

}

 

程式包括2個工作線程,一個負責處理訂單,計算訂單的總價,它在讀取配置信息時採取讀鎖。另一個負責更新配置信息,採用寫鎖。

public static void main(String[] args) throws Exception {
        final ConcurrentHashMap<String, AccountConfig> accountConfigMap =
                new ConcurrentHashMap<String, AccountConfig>();
        AccountConfig accountConfig1 = new AccountConfig(0.02, 0.05);
        accountConfigMap.put("user1", accountConfig1);
        AccountConfig accountConfig2 = new AccountConfig(0.03, 0.04);
        accountConfigMap.put("user2", accountConfig2);
        final ReadWriteLock lock = new ReentrantReadWriteLock();
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Runnable() {
            Random r = new Random();

            @Override
            public void run() {
                Long t1 = System.nanoTime();
                for (int i = 0; i < 100000000; i++) {
                    Order order = MockOrder();
                    lock.readLock().lock();
                    AccountConfig accountConfig = accountConfigMap.get(order.getUser());

                    double price = order.getPrice() * order.getCount() 
                            * (1 - accountConfig.getMembershipDiscount())
                            * (1 - accountConfig.getSpecialEventDiscount());

                    lock.readLock().unlock();
                }
                Long t2 = System.nanoTime();
                System.out.println("ReadWriteLock:" + (t2 - t1));
            }

            private Order MockOrder() {
                Order order = new Order();
                order.setUser("user1");
                order.setPrice(r.nextDouble() * 1000);
                order.setCount(r.nextInt(10));
                return order;
            }

        });

        executor.execute(new Runnable() {
            Random r = new Random();

            @Override
            public void run() {
                while (true) {
                    lock.writeLock().lock(); 
                    AccountConfig accountConfig = accountConfigMap.get("user1");
                    accountConfig.setMembershipDiscount(r.nextInt(10) / 100.0);
                    lock.writeLock().unlock();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        });
    }

 

方案二:採用不可變對象:

創建一個不可變的配置類ImmutableAccountConfig:

public final class ImmutableAccountConfig {
    
    private final double membershipDiscount;
    private final double specialEventDiscount;
    
    public ImmutableAccountConfig(double membershipDiscount, double specialEventDiscount)
    {
        this.membershipDiscount = membershipDiscount;
        this.specialEventDiscount = specialEventDiscount;
    }

    public double getMembershipDiscount() {
        return membershipDiscount;
    }

    public double getSpecialEventDiscount() {
        return specialEventDiscount;
    }
}

 

還是創建2個線程。訂單線程不必加鎖。而配置更新的線程由於採用了不可變類,採用替換對象的方式來更新配置:

public static void main(String[] args) throws Exception {
        final ConcurrentHashMap<String, ImmutableAccountConfig> immutableAccountConfigMap 
        = new ConcurrentHashMap<String, ImmutableAccountConfig>();
        ImmutableAccountConfig accountConfig1 = new ImmutableAccountConfig(0.02, 0.05);
        immutableAccountConfigMap.put("user1", accountConfig1);
        ImmutableAccountConfig accountConfig2 = new ImmutableAccountConfig(0.03, 0.04);
        immutableAccountConfigMap.put("user2", accountConfig2);

        //final ReadWriteLock lock = new ReentrantReadWriteLock();
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Runnable() {
            Random r = new Random();

            @Override
            public void run() {
                Long t1 = System.nanoTime();
                for (int i = 0; i < 100000000; i++) {
                    Order order = MockOrder();
                    ImmutableAccountConfig immutableAccountConfig = 
                            immutableAccountConfigMap.get(order.getUser());

                    double price = order.getPrice() * order.getCount()
                            * (1 - immutableAccountConfig.getMembershipDiscount())
                            * (1 - immutableAccountConfig.getSpecialEventDiscount());
                }
                Long t2 = System.nanoTime();
                System.out.println("Immutable:" + (t2 - t1));
            }

            private Order MockOrder() {
                Order order = new Order();
                order.setUser("user1");
                order.setPrice(r.nextDouble() * 1000);
                order.setCount(r.nextInt(10));
                return order;
            }

        });

        executor.execute(new Runnable() {
            Random r = new Random();

            @Override
            public void run() {
                while (true) {
                    //lock.writeLock().lock();
                    ImmutableAccountConfig oldImmutableAccountConfig = 
                            immutableAccountConfigMap.get("user1");
                    Double membershipDiscount = r.nextInt(10) / 100.0;
                    Double specialEventDiscount = 
                            oldImmutableAccountConfig.getSpecialEventDiscount();
                    ImmutableAccountConfig newImmutableAccountConfig = 
                            new ImmutableAccountConfig(membershipDiscount,
                            specialEventDiscount);
                    immutableAccountConfigMap.put("user1", newImmutableAccountConfig);
                    //lock.writeLock().unlock();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
}

(註:如果有多個寫進程,我們還是需要對他們加寫鎖,否則不同線程的配置信息修改會被相互覆蓋。而讀線程是不要加鎖的。)

 

結果:

ReadWriteLock:5289501171
Immutable    :3599621120

 

測試結果表明,採用不可變對象的方式要比採用讀寫鎖的方式快很多。但是,並沒有數量級的差距。

真實的項目環境的性能差別,還要以實際的項目測試為準。因為不同項目,讀寫線程的個數,負載和使用方式都是不一樣的,得到的結果也會不一樣。

 

設計上的優勢

採用不可變對象方式,相比讀寫鎖的好處還有就是在設計上的 -- 由於不可變對象的特性,我們不必擔心項目組的程式員會錯誤的使用配置類: 讀進程不用加鎖,所以不用擔心在需要被加讀鎖的地方沒有合理的加鎖,導致數據不一致性(但如果是多進程寫,還是要非常註意加寫鎖);也不用擔心配置在不被預期的地方被任意修改。

 

我們不能簡單地說,在任何場景下採用Immutable對象就一定比採用讀寫鎖的方式好, 還取決於讀寫的頻率、Immutable對象更新的代價等因素。但是我們可以通過這個例子,更清楚的理解採用Immutable對象的好處,並認真地在項目中考慮它,因為有可能為效率和設計帶來很大的好處。

 

google的不可變集合類庫

如果我們採用集合或者Map來保存不可變信息,我們可以採用google的不可變集合類庫(屬於Guava項目)。(JDK並沒有實現原生的不可變集合類庫)

http://mvnrepository.com/artifact/com.google.collections/google-collections/1.0

 

下麵寫一些代碼示例一下:

     public static void main(String[] args) throws Exception {
        //創建ImmutableMap
        ImmutableMap<String,Double> immutableMap = ImmutableMap.<String,Double>builder()
                .put("SpecialEventDiscount", 0.01)
                .put("MembershipDiscount", 0.02)
                .build();
        
        //基於原ImmutableMap生成新的更新的ImmutableMap
        Map<String,Double> tempMap = Maps.newHashMap(immutableMap);
        tempMap.put("MembershipDiscount", 0.03);
        ImmutableMap<String,Double> newImmutableMap = ImmutableMap.<String,Double>builder()
                .putAll(tempMap)
                .build();
    }

 

Binhua Liu原創文章,轉載請註明原地址http://www.cnblogs.com/Binhua-Liu/p/5573444.html


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

-Advertisement-
Play Games
更多相關文章
  • 用圖文形式全面介紹以最簡單的方法在 Raspberry Pi 2 上安裝 Windows 10 IoT Core 操作系統及搭建 VS2015 IoT 開發環境。 ...
  • 最近練習C#項目:何問起收藏夾(HoverTreeSCJ),實現編輯網址時,遇到這個問題:比如打開視窗後,要自動顯示數據。解決方法:那麼可以通過窗體的構造函數傳遞參數。比如窗體類: 那麼新建窗體實例時就可以通過參數id傳遞數值,在SetUrlId 方法中讀取載入數據到界面。例如 new Form_E ...
  • 這是我登陸後設置cookie的方法,本來cookieValueName是用FormsAuthentication.FormsCookieName替代的,突然有一天發總是得到null值,(目前情況也不明,可能是多個cookie的問題[也不太像,之前好好的,突然就變了])之後,直接改名傳固定值"CQSP ...
  • java常量池技術 java中常量池技術說的通俗點就是java級別的緩存技術,方便快捷的創建一個對象。當需要一個對象時,從池中去獲取(如果池中沒有,就創建一個並放入池中),當下次需要相同變數的時候,不用重新創建,從而節省空間。 java八種基本類型的包裝類和對象池 java中的基本類型的包裝類、其中 ...
  • Java程式 --創建游標包 --存儲過程 推薦:http://www.cnblogs.com/roucheng/p/3504465.html ...
  • 作為一個經驗豐富的C/C++程式員, 肯定親手寫過各種功能的代碼, 比如封裝過資料庫訪問的類, 封裝過網路通信的類,封裝過日誌操作的類, 封裝過文件訪問的類, 封裝過UI界面庫等, 也在實際的項目中應用過, 但是回過頭仔細想想,其實以前自己寫過的這些代碼,只能是在特定的項目或者特定的環境中使用, 對 ...
  • 相關:http://www.cnblogs.com/roucheng/p/cfenge.html ...
  • 一、GCC簡介: The GNU Compiler Collection,通常簡稱GCC,是一套由GNU開發的編譯器集,為什麼是編輯器集而不是編譯器呢?那是因為它不僅支持C語言編譯,還支持C++, Ada, Objective C等許多語言。另外GCC對硬體平臺的支持,可以所無所不在,它不僅支持X8 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...