写点什么

immutability 模式

用户头像
Geek_571bdf
关注
发布于: 2021 年 05 月 06 日

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) {
      //…… }
  }
}
复制代码
  1. 在使用 Immutability 模式时,需要注意以下两点:

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

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

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

final Map<Integer, Integer> map = new HashMap<>();map.put(1, 2); map.put(2, 3); 
复制代码


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


用户头像

Geek_571bdf

关注

还未添加个人签名 2019.06.13 加入

还未添加个人简介

评论

发布
暂无评论
immutability模式