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

 

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

 

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

 

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

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

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

 

解決方案

解決方案有兩種:ide

第一種是,採用ReadWriteLock。這是最多見的方式。性能

對讀操做加讀鎖,對寫操做加寫鎖。若是沒有正在發生的寫操做,讀鎖的代價很低。測試

 

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

1)對於訂單處理線程A來講,它再也不須要加鎖了!由於用於保存配置的對象是不可變對象。咱們要麼讀取的是一箇舊的配置對象,要麼是一個新的配置對象(新的配置對象覆蓋了舊的配置對象)。不會出現「髒讀」的狀況。ui

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

相關文章
相關標籤/搜索