Bootstrap

immutability模式

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

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

 

3. 要实现一个具备不可变性的类,需要三步:

①将类中所有的属性都用final修饰;

②只允许存在读方法,不允许写方法;

③类本身也通过final修饰,避免子类通过继承,覆盖父类中的方法。

 

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

 

4. 不过,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);
  }
}

5. 不过你也会发现,所有的修改操作都会创建一个新的对象,而对象创建得太多,反而会浪费内存。因此,这里利用到了享元模式来避免创建重复对象。像String和基础类型的包装类,都用到了享元模式。

享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑是这样的:创建对象时,首先在对象池中看看是否存在,如果存在,则直接返回对象池中的对象;否则,才去新建一个对象,并放入到对象池中。

 

6. 以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);
    }
}

 

7. 由于享元模式的存在,这也是为什么说“包装类和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) {

      //……
    }

  }

}
  • 对象的所有属性都是 final 的,并不能保证不可变性;

  • 不可变对象也需要正确发布。

如下所示,在 Java 语言中,final 修饰的属性一旦被赋值,就不可以再修改,但如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。因此,final的语义是引用一旦被赋值,将不可再指向其它引用。

final Map map = new HashMap<>();
map.put(1, 2); 
map.put(2, 3); 

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