并发编程设计模式--Immutability模式
Posted amberjava
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程设计模式--Immutability模式相关的知识,希望对你有一定的参考价值。
不变性(Immutability)模式
所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。
一、如何让一个类具有不变性呢
将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。实际上在Java SDK 经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性。通过看源码,可以发现他们的类和属性都是 final 的,所有方法均是只读的。
你或许有疑问,String类里面有很多方法是可以修改字符串的。看过底层源码的小伙伴一定知道,String的SubString() repleace()方法 都是会新创建对象的。
所有的修改操作都创建一个新的不可变对象,创建的对象太多了,太浪费内存,有没有什么办法可以解决呢?
二、利用享元模式可以减少创建对象的数量
如果你熟悉面向对象相关的设计模式,相信你一定能想到享元模式(Flyweight Pattern)。利用享元模式可以减少创建对象的数量,从而减少内存占用。Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。
以下是Long中源码,Long 内部维护了一个静态的对象池,仅缓存了 [-128,127] 之间的数字,这个对象池在 JVM 启动的时候就创建好了。 因为 Long 这个对象的状态共有 264 种,实在太多,不宜全部缓存,而 [-128,127] 之间的数字利用率最高
在Integer中也是一样的
面试曾经被问到,有个疑问,“Integer 和 String 类型等等 基础类型的包装类是适合做锁” ?不能,因为它们内部用到了享元模式,这会导致看上去私有的锁,其实是共有的。切记
三、使用 Immutability 模式的注意事项:
- 对象的所有属性都是 final 的,并不能保证不可变性;
- 不可变对象也需要正确发布。
在下面的代码中,Foo 具备不可变性,线程安全,但是类 Bar 并不是线程安全的,类 Bar 中持有对 Foo 的引用 foo,对 foo 这个引用的修改在多线程中并不能保证可见性和原子性。
1 //Foo 线程安全 2 final class Foo{ 3 final int age=0; 4 final int name="abc"; 5 6 } 7 8 //Bar 线程不安全 9 class Bar { 10 Foo foo; 11 void setFoo(Foo f){ 12 this.foo=f; 13 } 14 15 }
也就是说:不可变对象虽然是线程安全的,但是并不意味着引用这些不可变对象的对象就是线程安全的
如果你的程序仅仅需要 foo 保持可见性,无需保证原子性,那么可以将 foo 声明为 volatile 变量,这样就能保证可见性。如果你的程序需要保证原子性,那么可以通过原子类AtomicReference来实现,AtomicReference也是java.util.concurrent包下的类,跟AtomicInteger等是一样的,也是基于CAS无锁理论实现的,但是不同的是 AtomicReference 是操控多个属性的原子性的并发类。
来上源码,这里需要注意下,这里的比对两个对象,比对的方式不是equals
而是==
,意味着比对的是内存的中地址,这个我们可以通过unsafe.compareAndSwapObject()
方法查看,是一个native方法,调用计算机底层硬件实现的。
1 public class SafeWM { 2 //库存变化对象的上限、下限 3 class WMRange{ 4 final int upper; 5 final int lower; 6 WMRange(int upper,int lower){ 7 // 省略构造函数实现 8 } 9 10 } 11 //构造封装的原子类引用对象 12 final AtomicReference<WMRange>rf = new AtomicReference<>( new WMRange(0,0) ); 13 14 // 只设置库存上限 15 void setUpper(int v){ 16 while(true){ 17 WMRange old = rf.get(); 18 // 检查参数合法性 19 if(v < or.lower){ 20 throw new IllegalArgumentException(); 21 } 22 23 WMRange new = new WMRange(v, old.lower); 24 //cas关键方法 25 if(rf.compareAndSet(old, new)){ 26 27 return; 28 29 } 30 31 } 32 33 } 34 35 }
总结
利用 Immutability 模式解决并发问题,也许你觉得有点陌生,其实你天天都在享受它的战果。Java 语言里面的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。Immutability 模式是最简单的解决并发问题的方法,建议当你试图解决一个并发问题时,可以首先尝试一下 Immutability 模式,看是否能够快速解决。
以上是关于并发编程设计模式--Immutability模式的主要内容,如果未能解决你的问题,请参考以下文章
Day847.Immutability模式 -Java 性能调优实战
JUC并发编程 共享模式之工具 ThreadPoolExecutor -- 任务调度线程池 定时任务 / 延时执行(ScheduledThreadPoolExecutor 延时执行 / 定时执行)(代