Chapter 11. Concurrency
Item 78: Synchronize access to shared mutable data
同步访问可变的数据
synchronized
关键字保证了同一时间,只能够有单个线程去访问同步方法或者同步代码块。
同步,不仅避免了多个线程并发访问导致数据不一致的情况,同时也保证了每个进入同步块的线程,能够感知到之前线程对同步块的改变。
有很多人认为,在对一个原子变量(atomic variable)进行读写的时候,应该避免使用synchronize
以提升性能。但是这种说法真的是无稽之谈,因为,对原子变量的读写根本无法保证修改的可变性(visible)。而synchronize
可以保证线程之间的正确通信和互斥。
需要注意的是,当且仅当读写方法都有synchronize
修饰时,其可见性才会生效。
当然,除了使用synchronized
关键字之外,还有可以使用volatile
关键字来保证可见性。但是,volatile
关键字并不保证互斥性,也不保证原子性,它只是保证任何线程在访问被其修饰的变量时,都能够获取其最新的版本。
如何去保证原子性呢?第一种方法是使用synchronize
关键字,另一种方法是使用java.util.concurrent.atomic
包下原子类。这个包提供了无锁的、线程安全的基本数据类型。
Item 79: Avoidc excessive synchronization
避免过度同步
过度同步可能会导致性能衰退、死锁、甚至非确定性的行为。
首先,在同步区域里头,永远不要去调用一个不确定性的方法,例如用于被实现的抽象方法、或者由客户端控制的函数式方法等。
在同步区域之外调用外域方法被称为开放调用(open call)。开放调用不仅可以避免失败,还能提高系统的性能,因为你无法预测外域方法的性能和返回值。
总之,不要在同步代码块中去干太多的事情:
- 获得锁
- 检测共享数据
- 更新共享数据
- 释放锁
Item 80: Prefer executors, tasks, and Streams to threads
优先executors, tasks和stream而非threads
Executors FrameWork是一个很灵活的基于接口的任务执行工具。可以使用静态工厂类java.util.concurrent.Executors
创建各种类型的线程池。
对于线程池的选择是比较有技巧的,如果是小程序或者是轻量级的应用,则可使用CachedThreadPool
,因为其开箱即用,不需要配置。但是如果是重量级的应用,用这个线程池就捉襟见肘了,因为其会无限制地创建新的线程。最好用固定数目的线程池FixedThreadPool
。
尽量不要自己编写自己的工作队列,尽量不要直接使用线程。抽象的工作单元,即任务(task ),有两种形式,第一种是runnable
和其远方表亲callable
(callable可以返回值,并且抛出任意异常)
Item 81: Prefer concurrency utilities to wait and notify
并发工具优先于wait | notify
自从Java 5以来,平台提供了各种各样的并发工具类。鉴于想要正确使用wait | notify
的困难性,没有理由不使用这些优秀的工具类来替代wait | notify
java.util.concurrrent
并发工具包主要分为三大类,第一大类是执行器框架,其二是concurrent collections,其三是synchronizer。
需要在非并发场景下使用并发工具类,因为并发工具类中的锁(Lock)只会拖慢系统的运行效率。此外,数个方法组成的操作也是非原子的。因此,并发接口装配了一些状态独立修改操作,这些操作是通过合并几个初步的动作而得到的原子性的操作。
并发工具类的出现,直接让同步集合变得过时了。无脑使用并发集合替代同步集合即可提高并发应用的性能。
同步类的作用是协调线程之间的活动。最常用的同步器就是CountDownLatch
。
若要维护一下使用wait
的历史遗留代码,可以使用如下的方式,使用wait
方法:
java
synchronized(obj) {
while(condition dose not hold) {
obj.wait(); // realease lock, and requires on wakeup
}
}
需要注意,一定要将wait
方法写在循环里头,不要在循环外面去引用wait
方法。
关于唤醒线程的方法notify
和notifyAll()
,最保险的方法就是使用notifyAll
方法去唤醒所有的线程。
Item 82: Document thread safety
线程安全的文档化。
为了保证安全并发使用,一个类必须清晰地在文档中说明其支持的线程安全的等级:
- 不可变的
- 无条件的线程安全
- 有条件的线程安全
- 非线程安全
- 线程对立:非同步情况下修改静态数据
Item 83: Use Lazy initialization judiciously
谨慎地使用懒加载
懒加载是直到某个属性第一次被使用时才加载其值的行为。当某个字段的值永远没有被使用,则其永远也不会被初始化。
懒加载是一把双刃剑。当一个类的属性值只在类的部分实例中使用,并且其初始化代价比较大的时候,使用懒加载是一个比较好的优化方式。
但是,并发环境下,懒加载的使用需要注意技巧。需要使用同步锁对初始化过程进行同步,否则会产生严重的BUG。
谨记,大多数情况下,正常初始化要比延迟加载合适。
如果你非要对静态字段做懒加载性能优化的时候,一定要使用静态内部类Holder
方式初始化静态字段。其利用了"类在其使用的时候才会初始化"的特性。我们在单例模式的懒加载实现形式中也是使用了这种方法。
java
private static class FieldHolder {
static final FieldType field = cmomputeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
如果想要使用懒加载实例化去提升性能,在并发环境下一定要去使用双重检查锁(Double Check Lock)的形式。这种形式下,需要变量声明为volatile
。
总而言之,如果你非要去使用懒加载优化性能,对于不同类型的字段,选择合适的懒加载方式:
- 实例字段:双重检查锁
- 静态字段:
Holder
- 基本数据类型:能够忍受重复初始化的情况下,使用单重检查
Item 84: Don't depend on the thread scheduler
不要依赖于线程调度器
多线程情况下,线程调度器用来决定那个线程可以运行,运行多久。
任何依赖于线程调度器以达到正确性或者高性能目的的程序,很可能都是不可移植的。
最佳实践是,保证正在运行的线程数量小于处理器的数量。
线程不应该处于忙-等(busy-wait) 状态,即自旋状态。