immutability 模式
1.我们知道,多个线程同时读写同一共享变量存在并发问题。因此,如果只存在读操作,而没有写操作,自然就能保证线程安全。
2.解决并发问题的一种设计模式:不变性(Immutability)模式。所谓不变性,即对象一旦被创建之后,其状态就不再发生变化。
3. 要实现一个具备不可变性的类,需要三步:
①将类中所有的属性都用 final 修饰;
②只允许存在读方法,不允许写方法;
③类本身也通过 final 修饰,避免子类通过继承,覆盖父类中的方法。
事实上,我们常用的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果翻看源码,会发现这些类的类声明、属性和方法,都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的。
4. 不过,String 类也有诸如 replace() 字符串替换操作,这怎么能说所有的方法都是只读的呢?
String 类中使用 value[] 来存储字符,事实上,如果翻看源码就能发现,类似于像 replace()方法的实现,其实并没有修改 value[],而是将替换后的字符串作为返回值返回了。
因此,如果具备不可变性的类,需要提供类似修改的功能,方法就是 创建一个新的不可变对象。这也是与可变对象的一个重要区别,可变对象往往是修改自己的属性。
5. 不过你也会发现,所有的修改操作都会创建一个新的对象,而对象创建得太多,反而会浪费内存。因此,这里利用到了享元模式来避免创建重复对象。像 String 和基础类型的包装类,都用到了享元模式。
享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑是这样的:创建对象时,首先在对象池中看看是否存在,如果存在,则直接返回对象池中的对象;否则,才去新建一个对象,并放入到对象池中。
6. 以 Long 类为例。
Long 类并没有照搬享元模式,Long 内部维护了一个静态的对象池(LongCache),仅缓存了[-128,127]这 256 个数字,这个对象池在 JVM 启动时就创建好了,且这个对象池一直都不会变化,也就是说它是静态的。
7. 由于享元模式的存在,这也是为什么说“包装类和 String 类的对象 不适合做锁”。因为看上去它们好像是分别私有的锁,其实是共有的。如下所示,A 和 B 中分别单独持有锁 al 和 bl,看似是各自拥有的,但实际上 al 和 bl 是同一个对象,结果 A 和 B 是在共用一把锁。
在使用 Immutability 模式时,需要注意以下两点:
对象的所有属性都是 final 的,并不能保证不可变性;
不可变对象也需要正确发布。
如下所示,在 Java 语言中,final 修饰的属性一旦被赋值,就不可以再修改,但如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。因此,final 的语义是引用一旦被赋值,将不可再指向其它引用。
要安全的发布对象,需要保证引用的修改在多线程中可见性和原子性。如果只需要保证可见性,我们将引用用 volatile 修饰,如果需要确保原子性,我们可以使用原子引用类 AtomicReference。
评论