Java 同步工具与组合类的线程安全性分析


Java 同步工具与组合类的线程安全性分析

文章插图
 
何为线程安全的类?一般来说,我们要设计一个线程安全的类,要从三个方面去考虑:
  1. 构成状态的所有变量 。比如某个域是集合类型,则集合元素也构成该实例的状态 。
  2. 某些操作所隐含的不变性条件 。
  3. 变量的所有权,或称它是否会被发布 。
基于条件的同步策略不变性条件取决于类的语义,比如说计数器类的 counter 属性被设置为 Integer 类型,虽然其阈值在 Integer.MIN_VALUE 到 Integer.MAX_VALUE 之间,但是它的值必须非负 。即:随着计数的进行, conuter >= 0 总是成立 。
除了不变性条件之外,一些操作还需要通过后验条件,以此判断状态的更改是否有效 。比如一个计数器计到 17 时,它的下一个状态只可能是 18 。这实际涉及到了对原先状态的 "读 - 改 - 写" 三个连续的步骤,典型的如自增 ++ 等 。"无记忆性" 的状态是不需要后验条件的,比如每隔一段时间测量的温度值 。
先验条件可能是更加关注的问题,因为 "先判断后执行" 的逻辑到处存在 。比如说对一个列表执行 remove 操作时,首先需要保证列表是非空的,否则就应该抛出异常 。
在并发环境下,这些条件均可能会随着其它线程的修改而出现失真 。
状态发布与所有权在许多情况下,所有权和封装性是相互关联的 。比如对象通过 private 关键字封装了它的状态,即表明实例独占对该状态的所有权 ( 所有权意味控制权 ) 。反之,则称该状态被发布 。被发布的实例状态可能会被到处修改,因此它们在多线程环境中也存在风险 。
容器类通常和元素表现出 "所有权" 分离的形式 。比如说一个声明为 final 的列表,客户端虽然无法修改其本身的引用,但可以自由地修改其元素的状态 。这些事实上被发布的元素必须被安全地共享 。这要求元素:
  1. 自身是事实不可变的实例 。
  2. 线程安全的实例 。
  3. 被锁保护 。
实例封闭大多数对象都是组合对象,或者说这些状态也是对象 。对组合类的线程安全性分析大致分为两类:
  1. 如果这些状态线程不安全,那应该如何安全地使用组合类?
  2. 即使所有的状态都线程安全,是否可以推断组合类也线程安全?或者说组合类是否还需要额外的同步策略?
对于第一个问题,见下方的 Bank 代码,它模拟了一个转账业务:
class Bank {private Integer amount_A = 100;private Integer amount_B = 50;public synchronized void transaction(Integer amount){var log_0 = amount_A + amount_B;amount_A += amount;amount_B -= amount;var log_1 = amount_A + amount_B;assert log_0 == log_1;}}复制代码虽然 amount_A 和 amount_B 本身作为普通的 Integer 类型并不是线程安全的,但是它们具备线程安全的语义:
privatetransaction()也可以理解成: Bank 是为两个 Integer 状态提供线程安全性的容器 。在此处,同步策略由 synchronized 内置锁实现 。
编译器会在 synchronized 的代码区前后安插 monitorenter 和 monitorexit 字节码表示进入 / 退出同步代码块 。JAVA 的内置锁也称之监视器锁,或者监视器 。
至于第二个问题,答案是:看情况,具体地说是分析是否存在不变性条件,在这里,它指代在转账过程当中,a 和 b 两个账户的余额之和应当不变 。如果使用原子类型保护 amount_A 和 amount_B 的状态,那么是否就可以撤下 transaction() 方法上的内置锁了?
class UnsafeBank {private final AtomicInteger amount_A = new AtomicInteger(100);private final AtomicInteger amount_B = new AtomicInteger(50);public void transaction(Integer amount){amount_A.set(amount_A.get() - amount);amount_B.set(amount_B.get() + amount);}}复制代码transaction() 方法现在失去了锁的保护 。这样,某线程 A 在执行交易的过程中,另一个线程 B 也可能会 "趁机" 修改 amount_B 的账目 —— 这个时机发生在线程 A 执行 amount_B.get() 之后,但在 amount_B.set() 之前 。最终,B 线程的修改将被覆盖而丢失,在它看来,尽管两个状态均是原子变量,但不变性条件仍然被破坏了 。
由此得到一个结论 —— 就算所有的可变状态都是原子的,我们可能仍需要在封装类的层面进一步考虑同步策略,最简单直接的就是找出封装类内的所有复合操作:


推荐阅读