Effective Java 读书笔记(九):并发
同步访问共享的可变数据
- 当共享可变数据时,不进行同步有两个坏处:一是可能导致数据处于不一致的状态,二是一个线程的修改无法马上对其他线程可见。
- 同步带来两个效果:原子性、可见性。Synchronized 既能保证原子性,也能保证可见性。volatile 则只能保证可见性。
- 增量操作符 ++ 不是原子的。
- 共享不可变的数据是安全的,不需要加锁同步。
- 对于共享对象,不去修改其内容,只同步其引用,这种对象可以称为事实上不可变的。这种对象引用从一个线程传递到其他线程,被称为安全发布(safe publication)。
避免过度同步
为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,同时要尽量限制同步区域内部的工作量。
使用线程池管理线程,不要手动 new 线程
从 Java5 开始引入了 Executor Framework,这是一个灵活的基于接口的任务执行工具,执行的是 Runnable 或 Callable(后者有返回值)。
在实际做项目时,不推荐使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式。为什么呢?Executors 返回的线程池有一些缺点:
1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
代码不受约束是很可怕的事情。
从本质上将,Executor Framework 所做的工作是执行,犹如 Collections Framework 所做的工作是聚集一样。
并发工具优先于 wait 和 notify
wait 和 notify 有什么缺点呢?
1. 只有一个资源,但是可能用 notify all 唤醒了所有等待线程。
2. 在没有通知的情况下,等待线程也可能苏醒,被称为虚假唤醒 spurious wakeup。
wait 的标准用法如下:
synchronized (obj) {
while (condition does not hold) {
obj.wait(); // 释放锁,等待被唤醒
}
// 执行其他操作
}
始终使用 wait 循环模式调用 wait 方法,循环会在等待之前和之后测试条件。
在有现成的并发工具时,不要自己用 wait、notify 去控制并发,比如用 ConcurrentMap,好过自己控制并发。ConcurrentMap 相对于 Collections.synchronizedMap 性能更好。
线程安全性的文档化
当一个类的实例或静态方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立的约定的重要组成部分。如果没有在文档中描述类的并发性情况,使用这个类的程序员将不得不做出某些假设。如果假设错误,可能导致缺少同步,或过度同步。
线程安全有多种级别:
1. 不可变的 immutable:String、Long、BigInteger。
2. 无条件的线程安全:无需任何外部同步,比如 Random、ConcurrentHashMap。
3. 有条件的线程安全:部分方法需要外部同步,比如 Collections.synchronizedMap 的迭代器。
4. 非线程安全:比如 ArrayList 和 HashMap。
5. 线程对立的:外部同步也白搭的,不能线程安全地被多个线程并发使用。
在无条件的线程安全中,需要使用私有锁对象,防止外部客户端访问锁,影响对象的同步。私有锁对象特别适合于那些为继承而设计的类,如果使用自身实例作为锁,子类很容易在无意中妨碍基类的操作,反之亦然。出于不同的目的而使用相同的锁,子类和基类可能会互相绊住对方的脚。
有条件的线程安全,需要在文档中说明:在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。
慎用延迟初始化
是否要用延迟初始化,要看具体情况。延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销(会影响初次访问的性能)。若非必要,就不要使用延迟初始化。
一切都是权衡,很多服务器系统会在起到后,提前初始化一些大对象后,或者进行一定程度的预热后,再对外提供服务。
如果出于性能的考虑要对静态域使用延迟初始化,推荐使用 lazy initialization holder class 模式:
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getInstance() {
return FieldHolder.field;
}
如果出于性能考虑,要对实例域进行初始化,就使用双重检查模式 double check idiom:锁 + volatile。
private volatile FieldType field;
FieldType getInstance() {
FieldType result = field;
if (result == null) {
synchronized (this) {
result = field;
if (result == null) {
field = result = computeFieldValue();
}
}
}
return result;
}
局部变量 result 的使用,能够提升 25% 的性能。
如果可以接受实例域的重复初始化,可以使用单重检查模式 single check idiom:
private volatile FieldType field;
FieldType getInstance() {
FieldType result = field;
if (result == null) {
field = result = computeFieldValue();
}
return result;
}
去掉 volatile 可以加快某些架构上的访问,代价是增加了额外的初始化,这种模式叫 racy single check idiom,在 String 的散列码字段 hash 的初始化中用到。
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
不要依赖于线程调度器
任何依赖于线程调度器来达到正确运行或者性能要求的程序,很可能都是不可移植的。要编写健壮的、响应良好的、可移植的应用程序,最好的办法是设置合理的线程数目,减少无意义的工作(比如 busy-wait)。
不要依赖 Thread.yield 或线程优先级,它们不具有可移植性,也就是说在不同 JVM 上表现不同。
避免使用线程组 ThreadGroup
线程组的实现由很多问题,使用线程池 executor 即可。