解决并发问题,其实最简单的办法就是让共享变量只有读操做,而没有写操做。这个办法如此重要,以致于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式
。所谓不变性,简单来说,就是对象一旦被建立以后,状态就再也不发生变化
。换句话说,就是变量一旦被赋值,就不容许修改了(没有写操做);没有修改操做,也就是保持了不变性。设计模式
实现一个具有不可变性的类,仍是挺简单的。将一个类全部的属性都设置成 final 的,而且只容许存在只读方法,那么这个类基本上就具有不可变性了
。更严格的作法是这个类自己也是 final 的
,也就是不容许继承。由于子类能够覆盖父类的方法,有可能改变不可变性,因此推荐你在实际工做中,使用这种更严格的作法。缓存
Java SDK 里不少类都具有不可变性,只是因为它们的使用太简单,最后反而被忽略了。例如常常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具有不可变性,这些对象的线程安全性都是靠不可变性来保证的。若是你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵照不可变类的三点要求:类和属性都是 final 的,全部方法均是只读的
安全
咱们结合 String 的源代码来解释一下这个问题,下面的示例代码源自 Java 1.8 SDK,我略作了修改,仅保留了关键属性 value[] 和 replace() 方法,你会发现:String 这个类以及它的属性 value[] 都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串做为返回值返回了。多线程
public final class String { private final char value[]; // 字符替换 String replace(char oldChar, char newChar) { // 无需替换,直接返回 this if (oldChar == newChar){ return this; } int len = value.length; int i = -1; /* avoid getfield opcode */ char[] val = value; // 定位到须要替换的字符位置 while (++i < len) { if (val[i] == oldChar) { break; } } // 未找到 oldChar,无需替换 if (i >= len) { return this; } // 建立一个 buf[],这是关键 // 用来保存替换后的字符串 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } // 建立一个新的字符串返回 // 原字符串不会发生任何变化 return new String(buf, true); } }
经过分析 String 的实现,你可能已经发现了,若是具有不可变性的类,须要提供相似修改的功能,具体该怎么操做呢?作法很简单,那就是建立一个新的不可变对象
,这是与可变对象的一个重要区别,可变对象每每是修改本身的属性。并发
全部的修改操做都建立一个新的不可变对象,你可能会有这种担忧:是否是建立的对象太多了,有点太浪费内存呢?是的,这样作的确有些浪费,那如何解决呢?分布式
若是你熟悉面向对象相关的设计模式,相信你必定能想到享元模式(Flyweight Pattern)。利用享元模式能够减小建立对象的数量,从而减小内存占用。
Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。函数
享元模式本质上其实就是一个对象池
,利用享元模式建立对象的逻辑也很简单:建立以前,首先去对象池里看看是否是存在;若是已经存在,就利用对象池里的对象;若是不存在,就会新建立一个对象,而且把这个新建立出来的对象放进对象池里。性能
Long 这个类并无照搬享元模式,Long 内部维护了一个静态的对象池,仅缓存了 [-128,127] 之间的数字,这个对象池在 JVM 启动的时候就建立好了,并且这个对象池一直都不会变化,也就是说它是静态的。之因此采用这样的设计,是由于 Long 这个对象的状态共有 2**64 种,实在太多,不宜所有缓存,而 [-128,127] 之间的数字利用率最高。下面的示例代码出自 Java 1.8,valueOf() 方法就用到了 LongCache 这个缓存。this
Long valueOf(long l) { final int offset = 128; // [-128,127] 直接的数字作了缓存 if (l >= -128 && l <= 127) { return LongCache .cache[(int)l + offset]; } return new Long(l); } // 缓存,等价于对象池 // 仅缓存 [-128,127] 直接的数字 static class LongCache { static final Long cache[] = new Long[-(-128) + 127 + 1]; static { for(int i=0; i<cache.length; i++) cache[i] = new Long(i-128); } }
因此也就解释了 “Integer 和 String 类型的对象不适合作锁”,其实基本上全部的基础类型的包装类都不适合作锁,由于它们内部用到了享元模式,这会致使看上去私有的锁,实际上是共有的。线程
例如在下面代码中,本意是 A 用锁 al,B 用锁 bl,各自管理各自的,互不影响。但实际上 al 和 bl 是一个对象,结果 A 和 B 共用的是一把锁。
class A { Long al=Long.valueOf(1); public void setAX(){ synchronized (al) { // 省略代码无数 } } } class B { Long bl=Long.valueOf(1); public void setBY(){ synchronized (bl) { // 省略代码无数 } } }
在使用 Immutability 模式的时候,须要注意如下两点:
在 Java 语言中,final 修饰的属性一旦被赋值,就不能够再修改,可是若是属性的类型是普通对象,那么这个普通对象的属性是能够被修改的。例以下面的代码中,Bar 的属性 foo 虽然是 final 的,依然能够经过 setAge() 方法来设置 foo 的属性 age。因此,在使用 Immutability 模式的时候必定要确认保持不变性的边界在哪里,是否要求属性对象也具有不可变性
下面咱们再看看如何正确地发布不可变对象。不可变对象虽然是线程安全的,可是并不意味着引用这些不可变对象的对象就是线程安全的。例如在下面的代码中,Foo 具有不可变性,线程安全,可是类 Bar 并非线程安全的,类 Bar 中持有对 Foo 的引用 foo,对 foo 这个引用的修改在多线程中并不能保证可见性和原子性。
//Foo 线程安全 final class Foo{ final int age=0; final int name="abc"; } //Bar 线程不安全 class Bar { Foo foo; void setFoo(Foo f){ this.foo=f; } }
若是你的程序仅仅须要 foo 保持可见性,无需保证原子性,那么能够将 foo 声明为 volatile 变量,这样就能保证可见性。若是你的程序须要保证原子性,那么能够经过原子类来实现。下面的示例代码是合理库存的原子化实现,你应该很熟悉了,其中就是用原子类解决了不可变对象引用的原子性问题。
public class SafeWM { class WMRange{ final int upper; final int lower; WMRange(int upper,int lower){ // 省略构造函数实现 } } final AtomicReference<WMRange> rf = new AtomicReference<>( new WMRange(0,0) ); // 设置库存上限 void setUpper(int v){ while(true){ WMRange or = rf.get(); // 检查参数合法性 if(v < or.lower){ throw new IllegalArgumentException(); } WMRange nr = new WMRange(v, or.lower); if(rf.compareAndSet(or, nr)){ return; } } } }
利用 Immutability 模式解决并发问题,也许你以为有点陌生,其实你每天都在享受它的战果。Java 语言里面的 String 和 Long、Integer、Double 等基础类型的包装类都具有不可变性,这些对象的线程安全性都是靠不可变性来保证的。Immutability 模式是最简单的解决并发问题的方法,建议当你试图解决一个并发问题时,能够首先尝试一下 Immutability 模式,看是否可以快速解决。
具有不变性的对象,只有一种状态,这个状态由对象内部全部的不变属性共同决定。其实还有一种更简单的不变性对象,那就是无状态
。无状态对象内部没有属性,只有方法。在分布式领域,无状态意味着能够无限地水平扩展,因此分布式领域里面性能的瓶颈必定不是出在无状态的服务节点上。