immutability 模式

1.我们知道,多个线程同时读写同一共享变量存在并发问题。因此,如果只存在读操作,而没有写操作,自然就能保证线程安全。

2.解决并发问题的一种设计模式:不变性(Immutability)模式。所谓不变性,即对象一旦被创建之后,其状态就不再发生变化。

  1. 要实现一个具备不可变性的类,需要三步:
    ① 将类中所有的属性都用 final 修饰;
    ② 只允许存在读方法,不允许写方法;
    ③ 类本身也通过 final 修饰,避免子类通过继承,覆盖父类中的方法。

事实上,我们常用的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果翻看源码,会发现这些类的类声明、属性和方法,都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的。

  1. 不过,String 类也有诸如 replace() 字符串替换操作,这怎么能说所有的方法都是只读的呢?
    String 类中使用 value[] 来存储字符,事实上,如果翻看源码就能发现,类似于像 replace()方法的实现,其实并没有修改 value[],而是将替换后的字符串作为返回值返回了。
    因此,如果具备不可变性的类,需要提供类似修改的功能,方法就是 创建一个新的不可变对象。这也是与可变对象的一个重要区别,可变对象往往是修改自己的属性。
public final class String {
  private final char value[];
  String replace(char oldChar, char newChar) {
    if (oldChar == newChar){
      return this;
    }

    int len = value.length;
    int i = -1;
    /* avoid getfield opcode */
    char[] val = value; 
    //定位到需要替换的字符位置
    while (++i < len) {
      if (val[i] == oldChar) {
        break;
      }
    }
    //未找到oldChar,无需替换
    if (i >= len) {
      return this;
    } 
    // 新建一个buf[],用于保存替换后的字符串 
    char buf[] = new char[len];
    for (int j = 0; j < i; j++) {
      buf[j] = val[j];
    }
    while (i < len) {
      char c = val[i];
      buf[i] = (c == oldChar) ? newChar : c; // 全部替换
      i++;
    }
    // 创建一个新的字符串返回,而原字符串不会发生任何变化
    return new String(buf, true);
  }
}
  1. 不过你也会发现,所有的修改操作都会创建一个新的对象,而对象创建得太多,反而会浪费内存。因此,这里利用到了享元模式来避免创建重复对象。像 String 和基础类型的包装类,都用到了享元模式。
    享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑是这样的:创建对象时,首先在对象池中看看是否存在,如果存在,则直接返回对象池中的对象;否则,才去新建一个对象,并放入到对象池中。
  1. 以 Long 类为例。
    Long 类并没有照搬享元模式,Long 内部维护了一个静态的对象池(LongCache),仅缓存了[-128,127]这 256 个数字,这个对象池在 JVM 启动时就创建好了,且这个对象池一直都不会变化,也就是说它是静态的。
public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

// 这里的缓存等价于对象池
private static class LongCache {
    private LongCache(){}
    static final Long cache[] = new Long[-(-128) + 127 + 1]; //256
    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}
  1. 由于享元模式的存在,这也是为什么说“包装类和 String 类的对象 不适合做锁”。因为看上去它们好像是分别私有的锁,其实是共有的。如下所示,A 和 B 中分别单独持有锁 al 和 bl,看似是各自拥有的,但实际上 al 和 bl 是同一个对象,结果 A 和 B 是在共用一把锁。
class A {

  Long al=Long.valueOf(1);

  public void setAX(){

    synchronized (al) {

      //……

    }

  }

}

class B {

  Long bl=Long.valueOf(1);

  public void setBY(){

    synchronized (bl) {

      //……
    }

  }

}
  1. 在使用 Immutability 模式时,需要注意以下两点:
  • 对象的所有属性都是 final 的,并不能保证不可变性;
  • 不可变对象也需要正确发布。
    如下所示,在 Java 语言中,final 修饰的属性一旦被赋值,就不可以再修改,但如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。因此,final 的语义是引用一旦被赋值,将不可再指向其它引用。
final Map<Integer, Integer> map = new HashMap<>();
map.put(1, 2); 
map.put(2, 3); 

要安全的发布对象,需要保证引用的修改在多线程中可见性和原子性。如果只需要保证可见性,我们将引用用 volatile 修饰,如果需要确保原子性,我们可以使用原子引用类 AtomicReference。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容