读完第三章那些繁琐的术语和细节,头疼了整整一个星期。作者简直是苦口婆心,说得我如做梦一般。然而进入第四章,难度骤然降低,仿佛坐杭州的过山公交车突然下坡,鸟鸣花香扑面而来,看到了一片西湖美景。

从开始看书以来,无时无刻不体会着自学的痛苦。以前看一个大牛的博客,说自己换工作后现学Java,由于工作中有多线程的需求,于是开始看JCIP,只花了三天看完全书,三天!!他还表扬这本书写得好,说作者的想法跟他自己很像……我只好保持着微笑,踢自己两脚。

还有一次,跟一个互联网公司的朋友聊天,我说到自己看书很慢时,他安慰我说,他们公司有一个女生,看书看了两个月才勉强上手业务,可是隔壁的女生看了两天就已经能熟练地写框架代码了。前一个女生心理十分不平衡,于是找他排解。他分析道:另外那个女生是科班出身的,她实际看书的时间是四年本科加两天,而你只看了两个月,你已经比她聪明多了!听了朋友的安慰,我有点如释重负。然后朋友问我最近在看什么书,我说,有一本cs:app每次拿起来都读不下去又放下,断断续续也有一年多了才看到第三章。朋友轻描淡写地说:哦,那本啊,我一天就看完了。

为了活下去,我们还是来看看书。

设计线程安全类时的考量

如果要设计一个线程安全的类,至少要考虑到以下几点:

  1. 找出组成这个类对象的状态的变量们。
  2. 找出这些变量遵循的不变性
  3. 设计线程安全策略以安全访问对象的状态。

线程安全策略定义一个对象如何控制线程们对它的访问不违反不变性和后置条件。通常它是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:

 public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable { private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0; public synchronized int size() {
return count;
} public synchronized boolean isEmpty() {
return count == 0;
} public synchronized boolean contains(Object value) {
//internal logic
return false;
} //Other synchronized methods...
}

监视器模式有它的问题:

  1. 加锁粒度粗;
  2. client code也可以使用对象的固有锁,这样相当于加锁的机制散落在整个程序的各处,不好维护,且容易有很难发现的liveness问题。所以还是应该尽可能地使用private的对象当锁:
 public class PrivateLock{
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod(){
synchronized(myLock){
//access or modify widget
}
}
}

代理给线程安全类

在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:

 public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
} public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
} public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}

由于设计的人已经想好不变量为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()方法,而不破坏原有类的线程安全性。有四种办法:

  1. 直接改原有类的代码。当然这是在有权限的情况下。
  2. 继承原有类,然后按照原有的线程安全机制增加方法。这样做很脆弱,因为一旦原有类改变线程安全策略,子类会无声无息地break。
  3. client-side locking -- 在client code中用原有类中的锁进行保护。这样比2还脆弱,而且会引入耦合,实际上侵犯了“线程安全策略的封装”。
  4. 组合 -- 在一个新类中,将原有类对象作为域,加入新的方法并用自己的锁对所有方法进行保护,有点像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不是线程安全的,把程序员们吓尿了。

我们起码应该做到两件事:

  1. 记录线程安全的保证给client看。
  2. 记录线程安全的策略/机制给维护者看。

终于我们又平稳地度过了一个章节。看到西湖景色的你们还好吗?

[JCIP笔记](四)踩在巨人的肩上的更多相关文章

  1. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

  2. 《MFC游戏开发》笔记四 键盘响应和鼠标响应:让人物动起来

    本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9327377 作者:七十一雾央 新浪微博:http:// ...

  3. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  4. java之jvm学习笔记四(安全管理器)

    java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...

  5. Java学习笔记四---打包成双击可运行的jar文件

    写笔记四前的脑回路是这样的: 前面的学习笔记二,提到3个环境变量,其中java_home好理解,就是jdk安装路径:classpath指向类文件的搜索路径:path指向可执行程序的搜索路径.这里的类文 ...

  6. Java加密与解密笔记(四) 高级应用

    术语列表: CA:证书颁发认证机构(Certificate Authority) PEM:隐私增强邮件(Privacy Enhanced Mail),是OpenSSL使用的一种密钥文件. PKI:公钥 ...

  7. Learning ROS for Robotics Programming Second Edition学习笔记(四) indigo devices

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  8. Typescript 学习笔记四:回忆ES5 中的类

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  9. Django开发笔记四

    Django开发笔记一 Django开发笔记二 Django开发笔记三 Django开发笔记四 Django开发笔记五 Django开发笔记六 1.邮箱激活 users app下,models.py: ...

随机推荐

  1. 笔记:Maven 项目报告插件

    Maven 项目报告插件,都是对于前面生成的项目站点的内容丰富,因此都是基于项目站点的,生成的命令和生成项目站点一致(mvn site),项目报告插件的配置和一般插件不同,是在 project-> ...

  2. javaMail邮件发送功能(多收件人,多抄送人,多密送人,多附件)

    private Session session; private Transport transport; private String mailHost = ""; privat ...

  3. 通过修改然后commit的方式创建自己的镜像

    创建自己的镜像:通过现有的镜像来创建自己的镜像.1.首先拉取一个镜像到本地$ sudo docker imagesREPOSITORY          TAG                 IMA ...

  4. [luogu2831][noip d2t3]愤怒的小鸟_状压dp

    愤怒的小鸟 noip-d2t3 luogu-2831 题目大意:给你n个点,问最少需要多少条经过原点的抛物线将其覆盖. 注释:1<=点数<=18,1<=数据组数<=30.且规定 ...

  5. WPF笔记1 用VS2015创建WPF程序

    使用WPF创建第一个应用程序.实现功能如下: 单击"Red"按钮,文本显示红色:单击"Black"按钮,文本显示黑色:单击"Back"按钮, ...

  6. WEB 表格测试点

    Web页面的表格测试点: 1.表格列名 2.表格翻页.表格跳转到多少页.最后一页.首页 3.表格每页显示的数据, 数据的排序 4.表格无数据 5.表格支持的最大数据量 6.表格中数据内容超长时,显示是 ...

  7. SSH三大框架的整合

    SSH三个框架的知识点 一.Hibernate框架 1. Hibernate的核心配置文件 1.1 数据库信息.连接池配置 1.2 Hibernate信息 1.3 映射配置 1.4 Hibernate ...

  8. C语言第一次博客作业—输入输出

    一.PTA实验作业 题目1:7-3 温度转换 本题要求编写程序,计算华氏温度150°F对应的摄氏温度.计算公式:C=5×(F−32)/9,式中:C表示摄氏温度,F表示华氏温度,输出数据要求为整型. 1 ...

  9. c语言第五次作业--函数

    一.PTA实验作业 题目1.使用函数输出一个整数的逆序数 1.本题PTA提交列表 2.设计思路 1.int mod,rever:分别表示余数和返回的数 2.while(number%10 || num ...

  10. Python实现栈

    栈的操作 Stack() 创建一个新的空栈 push(item) 添加一个新的元素item到栈顶 pop() 弹出栈顶元素 peek() 返回栈顶元素 is_empty() 判断栈是否为空 size( ...