专栏首页屈定‘s Blog并行设计模式--immutable模式

并行设计模式--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 条评论
登录 后参与评论

相关文章

  • Spring -- 定时任务调度的发展

    Java领域的调度最早一般认为是Timer,接着由Quratz创造调度器(Scheduler)、任务(Job)和触发器(Trigger)三个核心概念后开始发展,...

    屈定
  • 工作--用户登录注册相关设计

    最近做一个网站,网站需要用户登录注册,自然也就需要一套高扩展性的用户模块设计,该篇文章记录笔者遇到问题的解决方案,希望对你有帮助。

    屈定
  • Spring Boot -- 自动配置原理

    在Spring Boot中自动配置一般使用@EnableXXX方式,Spring默认提供了@EnableAutoConfiguration来配置starter,...

    屈定
  • android实现App第一次进入时的引导学习界面

    因为我们所熟知的Android平台是一个又一个的Activity组成的,每一个Activity有一个或者多个View构成。所以说,当我们想显示一个界面的时候,我...

    战神伽罗
  • android 应用内部悬浮可拖动按钮简单实现代码

    本文介绍了android 应用内部悬浮可拖动按钮简单实现代码,分享给大家,具体如下:

    砸漏
  • 黑产大数据:手机黑卡调查

    手机黑卡似乎和大众没什么关系,但据说见过下面这张图的同学,每天的生活品质能提升30%。 ? 楔子 言归正传,作为一家严肃的安全公司,其实猎人君是来尝试解决这类问...

    FB客服
  • 聊聊rocketmq的pullFromWhichNodeTable

    rocketmq-all-4.6.0-source-release/client/src/main/java/org/apache/rocketmq/clien...

    codecraft
  • Java源码之ReentrantLock

    “ 上一篇文章分析了锁框架的AQS的源码,今天我们来分析一种具体的锁:重入锁ReentrantLock的源码,前面我们也说到ReentrantLock内部最重要...

    每天学Java
  • 【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

    前两篇对HashMap这家伙的主要方法,主要算法做了一个详细的介绍,本篇主要介绍HashMap中默默无闻地工作着的集合们,包括KeySet,val...

    弗兰克的猫
  • javascript数字格式化通用类——accounting.js使用

    简介 accounting.js 是一个非常小的JavaScript方法库用于对数字,金额和货币进行格式化。并提供可选的Excel风格列渲染。它没有依赖任何JS...

    Java中文社群_老王

扫码关注云+社区

领取腾讯云代金券