1.不变性(Immutability)模式:
简单来说,就是对象一旦被创建以后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性
2.如何快速实现具有不变性的类
具体实现如下:
- 将一个类的属性都设置为final,更严格的做法是将类都变成不可变的类,用final修饰
- 不存在写操作的方法,只有读操作的方法
例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的,String的写操作例如replace等方法会返回一个新的对象
当对Integer进行i操作时候,并发场景下也会出现线程安全的问题,但是为什么又说Integer是线程安全的呢?因为i实际上执行的并不是Integer的方法,而是Java编译器先执行拆箱再进行计算并装箱,就是说i执行累加的操作还是基于基本类型int的,因此Integer是线程安全的,因为Integer压根没有提供写方法,自然不会出现线程安全问题
如何针对不可变性的类,设计写方法?:创建一个新的不可变对象
但是如果调用一次写操作就创建一个对象,那么会占用大量内存,此时就可以使用享元模式(本质:对象池),利用享元模式可减少创建的对象,从而减少内存占用。Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式
用Long举例:
其内部维护了一个静态的对象池,缓存了[-128,127] 之间的数字,这个对象池在JVM启动时就创建好了,且对象池不会发生变化。
private static class LongCache {
static final Long[] cache = new Long[256];
private LongCache() {
}
static {
for(int var0 = 0; var0 < cache.length; ++var0) {
cache[var0] = new Long((long)(var0 - 128));
}
}
}
所以说Integer 和 String 类型的对象不适合做锁
因为由于对象池的存在,可能导致看上去不同的锁,其实是共享的,如下:
class A {
Long al=Long.valueOf(1);
public void setAX(){
synchronized (al) {
// 省略代码无数
}
}
}
class B {
Long bl=Long.valueOf(1);
public void setBY(){
synchronized (bl) {
// 省略代码无数
}
}
}
3.使用Immutability的注意事项
3.1.对象的所有属性都是final的,并不能保证不可变性
如果final修饰的成员变量不是不可变对象,那么还是会出现线程安全的问题,final修饰的成员变量对于应用类型而言,只是该引用变量无法修改,但是不代表该引用变量所指向的对象的属性无法更改,例子如下:
class Foo{
int age=0;
int name="abc";
}
final class Bar {
final Foo foo;
void setAge(int a){
foo.age=a;
}
}
3.2.正确的发布不可变对象
如果成员变量是线程安全的,但是未对其进行final修饰,那么该成员变量的可见性和原子性都无法得到保证,那么就可能出现可见性引发的一系列问题。
//Foo 线程安全
final class Foo{
final int age=0;
final int name="abc";
}
//Bar 线程不安全
class Bar {
Foo foo;
void setFoo(Foo f){
this.foo=f;
}
}