并发编程设计模式--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 模式的注意事项:

  1. 对象的所有属性都是 final 的,并不能保证不可变性;
  2. 不可变对象也需要正确发布。

在下面的代码中,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 延时执行 / 定时执行)(代

函数式编程的那些特性: Immutability

JVM的内存模式

Linux编程入门三网络编程六 事件处理模式和并发模式

并发编程领域的Thread-Per-Message设计模式到底是什么?