要编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。
「共享」意味着变量可以由多个线程同时访问,「可变」意味着变量的值在其生命周期可以发生变化。

一个对象可以被多个线程访问,那么要让它是线程安全的,需要采用同步机制。Java的主要同步机制:

  • 关键字synchronized
  • volatile类型的变量
  • 显式锁Explicit Lock
  • 原子变量

多个线程使用可变的状态变量可能会出现问题,解决办法有:

  • 不在线程间共享该变量
  • 将变量改为不可变变量
  • 在访问变量时使用同步

什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类始终都能变现出正确的行为,那么就称这个类是线程安全的。

无状态对象一定是线程安全的。

原子性

竞态条件

并发编程中,由于不恰当的执行时序而出现不正确的结果,称为竞态条件。

最常见的竞态条件为Check-Then-Act操作,即通过一个可能失效的观测结果来决定下一步的动作。
「延迟初始化」是常见的Check-Then-Act操作,判断一个对象是否已经初始化,如果没有,则初始化一个新的实例。

复合操作

包含了一组必须以原子方式执行的操作以确保线程安全性,称为复合操作。

当只有一个状态变量时,可以使用线程安全类来保证线程安全性。

加锁机制

当出现不止一个状态变量时,将变量都改为线程安全类,也未必能保证线程安全性。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

内置锁

Java提供了「同步代码块」(Synchronized Block)的内置锁机制来支持原子性。通过关键字synchronized来修饰。

1
2
3
synchronized (lock) {
// 代码
}

每个Java对象都可以做一个实现同步的锁,这些锁被称为内置锁。锁在线程进入同步代码块之前获得,退出代码块后释放。
在同一时间只能有一个线程持有这种锁,意味着后面的线程会被阻塞,直到前面的线程释放锁。

synchronized使用简单,却会造成比较低的性能。

重入

内置锁的粒度是「线程」,所以当一个线程试图获得一个已经由它自己持有的锁,是可以的。JVM会记录锁的使用者,并且为其计数,当计数值为0时,锁才会被释放。