• 无锁编程:采用不可变类减少锁的使用


    很多的同学很少使用、或者干脆不了解不可变类(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

  • 相关阅读:
    实现treeview的动态加载
    sql server2000中使用convert来取得datetime数据类型样式(全)
    一道微软公司的面试题目的算法实现
    后台一行输入太多内容,使前台显示自动换行方法
    在js中刷新本页
    关于datediff函数的用法
    C#中StringBuilder类的使用(转)
    在字符串中使用引号("")等字符 需要用转义字符\ 例如
    常用的SQL和TSQL语句(一) (转)
    JS弹出窗口的运用与技巧(转)
  • 原文地址:https://www.cnblogs.com/Binhua-Liu/p/5573444.html
Copyright © 2020-2023  润新知