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

从开始看书以来,无时无刻不体会着自学的痛苦。以前看一个大牛的博客,说自己换工作后现学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. eclipse打包

  2. 【Python】随机模块random & 日期时间のtime&&datetime

    ■ random 顾名思义,random提供了python中关于模拟随机的一些方法.这些方法都一看就懂的,不多说了: random.random() 返回0<n<=1的随机实数 rando ...

  3. PHP 环境搭建篇

    0x01 PHP 简介 PHP 是一种流行的通用脚本语言, 特别适合 web 开发. 快速, 灵活, 务实, PHP 的所有东西, 从你的博客到世界上最流行的网站. 0x02 PHP环境要求 Tips ...

  4. shiro(三),使用第三方jdbcRealm连接数据库操作

    这里采用第三方实现好的JdbcRealm连接数据库:首先来看一下源码: 接着前面的说:就把这个类当做我们自己写的就好了,我们需要实例化它,然后给他注入一个数据源 下面是ini文件配置 [main] # ...

  5. 云计算--网络原理与应用--20171122--STP与HSRP

    简单了解STP 学习HSRP 实验 一.  简单学习STP STP(spanning tree protocol)生成树协议,就是把一个环形的结构改变成一个树形的结构.通过一些算法,在逻辑上阻塞一些端 ...

  6. 听翁恺老师mooc笔记(9)--枚举

    枚举类型的定义 用符号而不是具体的数字来表示程序中的数字,这么表示的好处是可读性,当别人看你的程序,看到的是单词,很容易理解这些数字背后的含义,那么用什么符号来表示名字哪?需要const int常量的 ...

  7. HDFS之HA机制

  8. Beta冲刺 第二天

    Beta冲刺 第二天 1. 昨天的困难 由于前面的冲刺留下的问题很多,而且混乱的代码给我们接下来的完善工作带来了巨大的困难. 2. 今天解决的进度 潘伟靖: 1.对代码进行了review 2.为系统增 ...

  9. 201621123068 作业07-Java GUI编程

    1. 本周学习总结 1.1 思维导图:Java图形界面总结 2.书面作业 1. GUI中的事件处理 1.1 写出事件处理模型中最重要的几个关键词. 注册.事件.事件源.监听 1.2 任意编写事件处理相 ...

  10. 项目Beta冲刺Day1

    项目进展 李明皇 今天解决的进度 点击首页list相应条目将信息传到详情页 明天安排 优化信息详情页布局 林翔 今天解决的进度 前后端连接成功 明天安排 开始微信前端+数据库写入 孙敏铭 今天解决的进 ...