[JCIP笔记](四)踩在巨人的肩上
读完第三章那些繁琐的术语和细节,头疼了整整一个星期。作者简直是苦口婆心,说得我如做梦一般。然而进入第四章,难度骤然降低,仿佛坐杭州的过山公交车突然下坡,鸟鸣花香扑面而来,看到了一片西湖美景。
从开始看书以来,无时无刻不体会着自学的痛苦。以前看一个大牛的博客,说自己换工作后现学Java,由于工作中有多线程的需求,于是开始看JCIP,只花了三天看完全书,三天!!他还表扬这本书写得好,说作者的想法跟他自己很像……我只好保持着微笑,踢自己两脚。
还有一次,跟一个互联网公司的朋友聊天,我说到自己看书很慢时,他安慰我说,他们公司有一个女生,看书看了两个月才勉强上手业务,可是隔壁的女生看了两天就已经能熟练地写框架代码了。前一个女生心理十分不平衡,于是找他排解。他分析道:另外那个女生是科班出身的,她实际看书的时间是四年本科加两天,而你只看了两个月,你已经比她聪明多了!听了朋友的安慰,我有点如释重负。然后朋友问我最近在看什么书,我说,有一本cs:app每次拿起来都读不下去又放下,断断续续也有一年多了才看到第三章。朋友轻描淡写地说:哦,那本啊,我一天就看完了。
为了活下去,我们还是来看看书。
设计线程安全类时的考量
如果要设计一个线程安全的类,至少要考虑到以下几点:
- 找出组成这个类对象的状态的变量们。
- 找出这些变量遵循的不变性。
- 设计线程安全策略以安全访问对象的状态。
线程安全策略定义一个对象如何控制线程们对它的访问不违反不变性和后置条件。通常它是immutability、线程封闭和加锁的组合。
在第1步中,如果类中的变量全是基本类型,那么对象的状态就是这些变量的总和。比如说一个表示2D Point的类,它的状态就是它的 (x, y)值。但如果有非基本类型的变量那就厉害了,那么这个对象的状态就要算上这些变量能引用到的所有对象的域。比如,LinkedList的状态就包含所有它里面的元素的状态。
其实涉及到集合类时,通常大家都会遵循的一种规则是split ownership,即集合负责保证它自身的线程安全性,但client code需要保证集合中的对象的线程安全。比如servlet框架中的ServletContext类:
ServletContext类本身 | 线程安全的;使用setAttribute()和getAttribute()不需要额外加锁 |
ServletContext中存储的对象 | application需要自己保证其线程安全性(做成线程安全/effectively immutable/加锁访问) |
在第2步中,我们再来强调一下不变性是什么。不变性定义了某些状态是有效的,某些是无效的。比如我现在有一个Counter类,它唯一的域是一个long类型的counter。基于它的用途(计数器),虽然counter的范围理论上可以达到[Long.MIN_VALUE, Long.MAX_VALUE],但实际上我们规定它必须大于等于0。这就是Counter的不变性。
如果一个操作会产生*无效*的中间态,那么这个操作必须做成原子的。
如果有多个变量参与不变性,那么对这些变量的读/写必须都放在一起,加锁做成原子操作。
实例封闭 (Instance Confinement)
封装可以把对类中变量的访问控制到有限的方法调用,从而让线程安全的设计变得更简单,加锁的机制也更灵活。
作者努力推行封装,其实封装并不是线程安全的保证,只是一种有益的编程习惯而已。就好像买了健身服也不意味着你马上会瘦,但能让你瘦身的过程更加舒服和顺利。
Java监视器模式 (the Java Monitor Pattern)
Java监视器模式是一种比较简单的实例封闭。这个模式要求封装类中的所有状态变量,并在访问它们时用对象的固有锁进行保护。
许多java类库都用了Java监视器模式,如Vector和Hashtable:
1 public class Hashtable<K,V> extends Dictionary<K,V> 2 implements Map<K,V>, Cloneable, java.io.Serializable { 3 4 private transient Entry<?,?>[] table; 5 private transient int count; 6 private int threshold; 7 private float loadFactor; 8 private transient int modCount = 0; 9 10 public synchronized int size() { 11 return count; 12 } 13 14 public synchronized boolean isEmpty() { 15 return count == 0; 16 } 17 18 public synchronized boolean contains(Object value) { 19 //internal logic 20 return false; 21 } 22 23 //Other synchronized methods... 24 }
监视器模式有它的问题:
- 加锁粒度粗;
- client code也可以使用对象的固有锁,这样相当于加锁的机制散落在整个程序的各处,不好维护,且容易有很难发现的liveness问题。所以还是应该尽可能地使用private的对象当锁:
1 public class PrivateLock{ 2 private final Object myLock = new Object(); 3 @GuardedBy("myLock") Widget widget; 4 void someMethod(){ 5 synchronized(myLock){ 6 //access or modify widget 7 } 8 } 9 }
代理给线程安全类
在Java中,大部分对象都是组合对象 (Composite Object)。如果组合对象中的变量本身已经是线程安全的了,是否还要在组合对象中加一层线程安全的机制呢?答案是“看情况”。
- 如果组合对象A的状态只由某一个线程安全的对象B的状态组成,那么A可以放心地把线程安全这个责任代理给B。
- 如果组合对象A的状态由线程安全的对象B, C, D...组成,且它们是互相独立的,即不存在包含它们的不变量,则A可以把线程安全的责任代理给B, C, D。比如,一个VisualComponent类中可以有keyListeners和mouseListeners,它们相互独立。可以将它们都设置成CopyOnWriteArrayList,然后把addKeyListener(), removeKeyListener(), addMouseListener(), removeMouseListener()分别代理给它们。
- 如果组合对象A的状态由线程安全的对象B, C, D...组成,且存在包含它们的不变量,则A不可以把线程安全的责任代理给B, C, D,而需要多加一层线程安全机制。
大部分实际场景符合第三种。比如下面的NumberRange:
1 public class NumberRange { 2 // INVARIANT: lower <= upper 3 private final AtomicInteger lower = new AtomicInteger(0); 4 private final AtomicInteger upper = new AtomicInteger(0); 5 6 public void setLower(int i) { 7 // Warning -- unsafe check-then-act 8 if (i > upper.get()) 9 throw new IllegalArgumentException("can't set lower to " + i + " > upper"); 10 lower.set(i); 11 } 12 13 public void setUpper(int i) { 14 // Warning -- unsafe check-then-act 15 if (i < lower.get()) 16 throw new IllegalArgumentException("can't set upper to " + i + " < lower"); 17 upper.set(i); 18 } 19 20 public boolean isInRange(int i) { 21 return (i >= lower.get() && i <= upper.get()); 22 } 23 }
由于设计的人已经想好不变量为lower <= upper,必然有每次设定lower和upper时,都需要先检查这个不变量是否满足。然而这个检查的过程注定了setLower()和setUpper()是两个复合操作,容易产生竞态条件,比如线程1调用setUpper()时,检查到lower为5,接着将upper由10置为7;而线程2在线程1置upper之前检查到upper为原来的值10,于是将lower置为小于10的8,结果是lower = 8, upper = 7,不符合事先规定的不变性。所以NumberRange必须额外增加一层线程安全机制,即将setUpper()和setLower()设置成原子操作。
给现有线程安全类加方法
假如我们要给Vector加一个putIfAbsent()方法,而不破坏原有类的线程安全性。有四种办法:
- 直接改原有类的代码。当然这是在有权限的情况下。
- 继承原有类,然后按照原有的线程安全机制增加方法。这样做很脆弱,因为一旦原有类改变线程安全策略,子类会无声无息地break。
- client-side locking -- 在client code中用原有类中的锁进行保护。这样比2还脆弱,而且会引入耦合,实际上侵犯了“线程安全策略的封装”。
- 组合 -- 在一个新类中,将原有类对象作为域,加入新的方法并用自己的锁对所有方法进行保护,有点像Collections.synchronzied()方法或者Java监视器模式。这种方法不那么脆弱,因为不管原有类对象是否线程安全,新类都有自己的线程安全机制来保证线程安全。
第4种方法的例子:
@ThreadSafe public class ImprovedList<T> implements List<T> { private final List<T> list; public ImprovedList(List<T> list) { this.list = list; } public synchronized boolean putIfAbsent(T x) { boolean contains = list.contains(x); if (contains) list.add(x); return !contains; } public synchronized boolean add(T e) { return list.add(e); } public synchronized boolean remove(Object o) { return list.remove(o); } // ... similarly delegate other List methods }
记录线程安全策略
这是一件相对容易做到,且性价比比较高,却很少人去做的事。
就连Java类库的官方文档做得都不是很好。比如说,直到JDK1.4,java官方文档才说明java.text.SimpleDateFormat不是线程安全的,把程序员们吓尿了。
我们起码应该做到两件事:
- 记录线程安全的保证给client看。
- 记录线程安全的策略/机制给维护者看。
终于我们又平稳地度过了一个章节。看到西湖景色的你们还好吗?