不少的同窗不多使用、或者干脆不了解不可变类(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对象的好处,并认真地在项目中考虑它,由于有可能为效率和设计带来很大的好处。
若是咱们采用集合或者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