并行设计模式--immutable模式

线程不安全的原因是共享了变量且对该共享变量的操作存在原子性、可见性等问题,因此一种解决思路就是构造不可变的对象,没有修改操作也就不存在并发竞争,自然也不需要额外的锁,同步等操作,这种设计叫做immutable object模式,本文主要理解这种模式,并学习如何实现一个较好的immutable类。

immutable设计原则

一个比较严格的immutable模式,有如下几种设计原则(来自Java多线程编程实战指南

  1. 类本身是final修饰,防止其子类改变其定义的行为
  2. 所有字段都是用final修饰,是用final修饰不仅可以从语义上说明被修饰字段的引用不可改变,更重要的是这个语义在多线程环境下由JMM(Java内存模型)保证了被引用字段的初始化安全。即final修饰字段在其他线程可见时,其必须初始化完成。
  3. 在对象的创建过程中,this指针没有泄露给其他对象。防止其他对象在创建过程中对其进行修改。
  4. 任何字段,若其引用了其他可改变字段,其必须使用private修饰,并且该字段不能向外暴露,如有相关方法返回该值,则使用防御性拷贝。

immutable设计陷阱

不可变类经常会遇到以下陷阱,他是不可变的吗?答案当然不是,该类本身是不可变的,但是其内部引用的Date对象可变,调用方可以获取Date之后调用其set方法改变其指向的时间,最终导致该类变化,这种设计过程中经常遇到的一个问题。

public final class Interval {

  private final Date start;

  private final Date end;
  // 传入的是引用,因此共享了内存,导致Date可变。
  public Interval(Date start, Date end) {
    this.start = start;
    this.end = end;
  }

  public Date getStart() {
    return start;
  }

  public Date getEnd() {
    return end;
  }
  
}

一般解决思路是使用防御性拷贝,也就是要赋值的地方都重新创建对象,如下所示。

public final class Interval {

  private final Date start;

  private final Date end;

    // 进行防御性拷贝
  public Interval(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
  }
    // 对外接口仍然需要拷贝
  public Date getStart() {
    return new Date(start.getTime());
  }

  public Date getEnd() {
    return new Date(end.getTime());
  }

}

immutable设计举例

JDK8日期类

在JDK8中新增关于时间日期的一批API ,其设计均采用immutable模式,主要体现在一旦该实例被创建之后,不会提供修改的方法,每次需要进行操作时返回的总是一个新的类,也因此该类是线程安全的。

public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
    /**
     * The year.
     */
    private final int year;
    /**
     * The month-of-year.
     */
    private final short month;
    /**
     * The day-of-month.
     */
    private final short day;
    
    ....
    // 对其的操作总会返回一个新的实例
  public LocalDate plusDays(long daysToAdd) {
        if (daysToAdd == 0) {
            return this;
        }
        long mjDay = Math.addExact(toEpochDay(), daysToAdd);
        return LocalDate.ofEpochDay(mjDay);
    }
}

immutable与享元模式

immutable模式最大的弊端是产生了很多对象,比如上述JDK8的日期类,每一步修改操作都要产生一个中间对象,在很多情况下是可以利用享元模式来较少对象创建次数,事实上享元模式并没有要求所共享的实例一定是不可变的,只是在大多数情况不可变会使得享元模式更加简单纯粹。比如系统中有表示用户一次下单购买商品数量的类Quantity,那么考虑到用户一次性购买数量很少大于10,因此这个类设计成immutable并且应用享元模式就可以很好地提高性能。 其本身就是类似Integer类,因此设计的具体做法就非常类似Integer的实现(之所以在实现一遍,是为了更好的语义描述),对外提供两个创建入口,1是构造函数,构造函数直接创建出该类。2是valueOf静态方法,该方法会先去缓存中查询是否包含,包含则直接返回。当然也可以在该类中加一些关于数量本身限制判断的业务方法。

public class Quantity {

  private final int value;

  /**
   * 提供创建不可变类
   */
  public Quantity(int value) {
    this.value = value;
  }

  /**
   * 提供享元模式复用类
   */
  public static Quantity valueOf(int value) {
    if (null != QuantityCache.cache[value-1]) {
      return QuantityCache.cache[value-1];
    }
    return new Quantity(value);
  }

  private static class QuantityCache {
    static final int low = 1;
    static final int high = 10;
    static final Quantity cache[];

    static {

      cache = new Quantity[(high - low) + 1];
      int j = low;
      for(int k = 0; k < cache.length; k++)
        cache[k] = new Quantity(j++);

    }

    private QuantityCache() {}
  }
}

immutable与Builder模式

immutable有可能面临创建所需要过多的参数以及步骤,导致设计该类时需要提供很多与类本身没必要的方法,因此比较好的解决方案是利用Builder模式创建实例,Builder模式的本质是把对象本身提供的操作与对象的创建分离开,为客户端提供一个较易操作的方式去得到类的实例。

Builder模式配合中,对应的目标类往往只需要提供私有的构造函数,以及属性的get方法,构造过程则交给内部的Builder类来完成,这是一种对于过多参数或者构造之后很少变动的类所采取的一种比较好的方式。

public final class AsyncLoadConfig {
  /**
   * 模板执行任务所需要的线程池
   */
  @Getter
  private ExecutorService executorService;
  /**
   * 单个方法默认超时时间
   */
  @Getter
  private Long defaultTimeout;

  private AsyncLoadConfig(ExecutorService executorService, Long defaultTimeout) {
    this.executorService = executorService;
    this.defaultTimeout = defaultTimeout;
  }

  /**
   * 相关配置的建造器
   * @return
   */
  public static AsyncLoadConfigBuilder builder() {
    return new AsyncLoadConfigBuilder();
  }

  public static class AsyncLoadConfigBuilder {

    private Long defaultTimeout;

    private ExecutorService executorService;

    public AsyncLoadConfigBuilder defaultTimeout(Long defaultTimeout) {
      this.defaultTimeout = defaultTimeout;
      return this;
    }

    public AsyncLoadConfigBuilder executorService(ExecutorService executorService) {
      this.executorService = executorService;
      return this;
    }

    public AsyncLoadConfig build() {
      Assert.notNull(executorService, "executorService can't be null");
      Assert.notNull(defaultTimeout, "defaultTimeout can't be null");
      return new AsyncLoadConfig(executorService, defaultTimeout);
    }
  }
}

JDK中的CopyOnWrite容器

在JDK1.5之后提供了CopyOnWriteArrayListCopyOnWriteArraySet容器,这类容器并不是严格意义上的不可变,但是其是immutable思想的一种应用,其本质是每次添加都重新创建一个底层数组,把之前的数据拷贝过来,然后把要添加的数据添加到尾部,最后更新这个数组的引用,实现关键点时更新数组引用是一个原子性操作,因此所有读线程将始终看到数组处于一致性状态,那么这个数组就可以理解为immutable的一种实现,一旦创建后不再改变。

public boolean add(E e) {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
      Object[] elements = getArray();
      int len = elements.length;
      // 创建新数组,并拷贝旧元素
      Object[] newElements = Arrays.copyOf(elements, len + 1);
      // 设置新增的元素
      newElements[len] = e;
      // 设置新数组
      setArray(newElements);
      return true;
  } finally {
      lock.unlock();
  }
}

参考

Java多线程编程实战指南

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据科学

redis流计算

使用了tornado的异步和streamz的流处理两个库,需要redis 5.0以上版本

25550
来自专栏技术小黑屋

Java中的堆和栈的区别

当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更...

18030
来自专栏专注 Java 基础分享

深入理解Struts2----类型转换

     之前的一系列文章主要介绍了有关Struts2的一些基本用法和部分的简单原理,但是始终没有介绍有关拦截器的相关内容,从本篇开始我们将从另一个角度去深入理...

23690
来自专栏java技术学习之道

Java设计模式——代理模式实现及原理

14230
来自专栏从零开始学 Web 前端

09 - JavaSE之线程

PS: 如果我们没有 new一个 Thread 对象出来,而是直接使用 MyThread 的 run 方法(mt.run()),这就是方法调用,而不是启动线程了...

15050
来自专栏Java帮帮-微信公众号-技术文章全总结

【大牛经验】探讨Java的异常与错误处理

探讨Java的异常与错误处理 ENTER TITLE ? Java中的异常处理机制已经比较成熟,我们的Java程序到处充满了异常的可能,如果对这些异常不做预先的...

38660
来自专栏菜鸟计划

angularjs promise详解

一、什么是Promise Promise是对象,代表了一个函数最终可能的返回值或抛出的异常,就是用来异步处理值的。 Promise是一个构造函数,自己身上有al...

32050
来自专栏雨尘分享

4.Block的类型

17050
来自专栏IMWeb前端团队

Zepto核心模块之工具方法拾遗

本文作者:IMWeb 谦龙 原文出处:IMWeb社区 未经同意,禁止转载 前言 平时开发过程中经常会用类似each、map、forEach之类的方法...

29760
来自专栏Spark生态圈

[Spark SQL] 源码解析之Parser

Parser就是将SQL字符串切分成一个个Token,再根据一定语义规则解析为一棵语法树。我们写的sql语句只是一个字符串而已,首先需要将其通过词法解析和语法解...

37220

扫码关注云+社区

领取腾讯云代金券