前言
感谢王宝令老师的并发编程系列课程
背景
我们曾经说过:多个线程同时读写一个共享变量存在并发问题。这里的必要条件之一是读写,如果只有读,而没有写,是没有并发问题的。
解决并发问题,其实最简单的就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于上升到了一种解决并发问题的模式:不变性(Immutability)模式。 所谓不变性,简单来讲 就是对象一旦被创建之后,状态就不再发生变化,换句话说,就是变量 一旦 被赋值,就不允许修改(没有写操作);没有修改操作,,也就是保证了不变性。
快速实现具有不可变性的类
实现一个具备不可变性的类,还是挺简单的,将一个类的所有属性都设置成final的,并且只允许存在只读方法,这个类基本上就具备了不可变性了。更严格的做法是这个类也定义成final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中使用这种更严格的做法。
Java SDK 里很多类都具备不可变性,只是由于他们的使用太简单,最后反而被忽略了,例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类,都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。他们都遵守不可变性的三点要求 :类和属性都是final 的,所有方法都是只读的。
看到这里你可能会疑惑,Java 的 String 方法也有类似字符替换操作,怎么能说所有方法都是只读的呢? 打开源码看下,返回的其实是创建了一个新的不可变 字符串,没有在原来的字符串做操作。这是与可变对象一个重要区别,可变对象往往是修改自己的属性。
所有的修改操作都创建一个对象,是不是创建的对象太多了?是不是有点浪费内存?
利用享元模式避免创建重复对象
如果你熟悉面向对象的设计模式,你一定熟悉享元模式,利用享元模式可以减少对象创建的数量,从而减少内存占用。Java 语言里的Long、Integer、Short、Byte 等这些基本数据类型,都用到了享元模式。
下面我们以 Long 为例,看看如何通过享元模式来优化对象的创建。
享元模式本质上就是一个对象池,利用享元模式创建对象的逻辑也很简单,:创建之前首先去对象池里面看看是不是存在,如果已经存在就利用对象池里面的数据,如果不存在就会创建一个新对象,并且把这个新创建的对象放入对象池里面。
Long 这个类并没有照搬享元模式,Long 内部维护了一个静态的对象池,仅缓存了 [-128,127] 之间的数字, 这个对象池在JVM启动的时候就创建好了,而且这个对象池一直都不会发生变化,也就是说他是静态的。之所以采用这种设计,是因为,Long 这个对象的状态有2的64次方,实在太多,不宜缓存,而[-128,127] 之间的数字利用率最高, 可以参考 Long valueOf 方法的具体实现代码。
前面我们提到“Integer 和 String 类型的对象不适合做锁”, 其实基本上,所有的基础类型的包装类都不适合做锁,其实是共有的。
使用Immutability 模式注意事项
1、对象的所有属性都是final 的,并不能保证不可变行
2、不可变对象也要正确发布
在Java 语言中,final 修饰的属性一旦被赋值,就不可以在修改,但是如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。所以使用的时候,一定要明确使用的不可变界限在哪里,是否要求不可变对象属性也具备不可变行。
总结
利用Immutability 模式解决并发问题,是最简单 最粗暴的保证线程安全性的方式,如果你试图解决一个并发问题,可以首先尝试一下Immutability 模式,看能否快速解决问题。
具备不可变对象,只有一种状态,这个状态由对象内部所有的不变属性共同决定。其实还有一种更简单的不变对象,那就是无状态。无状态对象内部没有没有属性,只有方法。除了无状态对象,你还听过无状态服务,无状态协议。无状态好处如下:
1、核心点就是性能,无状态不需要做任何同步处理,自然性能很好。
2、在分布式领域,无状态意味着可以无限水平扩展,所以分布式领域性能瓶颈一定不是出在无状态服务节点上。