线程封闭
ad-hoc线程封闭
Ad-hoc 线程封闭描述了何时维护线程封闭的责任完全落在实现上。临时线程限制可能很脆弱,因为没有任何语言功能(例如可见性修饰符或局部变量)有助于将对象封闭在目标线程中。
线程封闭的一个特例适用于 volatile 变量。只要确保仅从单个线程写入 volatile 变量,就可以安全地对共享 volatile 变量执行读取-修改-写入操作。
由于其脆弱性,应谨慎使用Ad-hoc 线程封闭;如果可能,请改用一种更强的线程封闭形式(栈封闭或 ThreadLocal)
栈封闭
栈封闭是线程封闭的一种特殊情况,其中只能通过局部变量访问对象。正如封装可以更容易地保留不变量一样,局部变量可以更容易地将对象限制在线程中。局部变量本质上仅限于执行线程;它们存在于执行线程的栈中,其他线程无法访问。
ThreadLocal
一种更正式的维护线程封闭的方法是 ThreadLocal,它允许您将每个线程的值与一个值的持有对象相关联。ThreadLocal 提供了 get 和 set 访问器方法,它们为使用它的每个线程维护一个单独的值副本,因此 get 返回从当前执行的线程传递给 set 的最新值。
ThreadLocal变量通常用于防止在基于可变单例或全局变量的设计中共享。
例如,单线程应用程序可能会维护一个在启动时初始化的全局数据库连接,以避免必须将连接传递给每个方法。由于 JDBC 连接可能不是线程安全的,因此在没有额外协调的情况下使用全局连接的多线程应用程序也不是线程安全的。通过使用 ThreadLocal 来存储 JDBC 连接,每个线程将拥有自己的连接。
与全局变量一样,ThreadLocal变量会降低可重用性并在类之间引入隐藏的耦合,因此应谨慎使用。
不变性
另一个绕过同步需求的方法是使用不可变对象。到目前为止,我们描述的几乎所有原子性和可见性危害,例如看到陈旧的值、丢失更新或观察到对象处于不一致状态,都与同时尝试访问相同可变状态的多个线程的变化无常有关。如果无法修改对象的状态,这些风险和复杂性就会消失。
不可变对象始终是线程安全的。
不可变对象很简单。它们只能处于一种状态,由构造函数控制(初始化)。
不变性不等同于简单地将对象的所有字段声明为 final。字段全部为 final 的对象可能仍然是可变的,因为 final 字段可以保存对可变对象的引用。
如果一个对象是不可变的:
• 它的状态在构造后不能被修改;
• 它的所有字段都是final的;
• 它是正确构造的(this 引用在构造过程中不会逃逸)。
final字段
一个“大部分不可变”但具有一个或两个可变状态变量的对象仍然比具有许多可变变量的对象更简单。声明字段为 final 还向维护者表明这些字段不应更改。
正如将所有字段设为私有是一个好习惯,除非它们需要更高的可见性,将所有字段设为 final 是一个好习惯,除非它们需要可变。
使用 volatile 发布不可变对象
通过使用不可变对象来保存所有变量,可以消除访问或更新多个相关变量时的竞态条件。
安全发布
任何线程都可以安全地使用不可变对象而无需额外的同步,即使不使用同步来发布它们也是如此。
如果 final 字段引用可变对象,则仍然需要同步才能访问它们引用的对象的状态。
为了安全地发布一个对象,对象的引用和对象的状态必须同时对其他线程可见。正确构造的对象可以通过以下方式安全发布:
• 从静态初始化程序初始化对象引用;
• 将对它的引用存储到volitile字段或AtomicReference 中;
• 将对它的引用存储到正确构造的对象的final字段中;
• 将对它的引用存储到一个由锁适当保护的字段中。
事实不可变对象
如果对象在发布后不会被修改,那么对于其它在没有额外同步的情况下安全的访问这些对象的线程来说,安全发布是足够的。
技术上不是不可变的对象,但其状态在发布后不会被修改,称为事实不可变对象。
可变对象
如果一个对象在构造后可能被修改,安全发布只确保发布状态的可见性。同步不仅要用于发布一个可变对象,还必须在每次访问该对象时确保后续修改的可见性。要安全地共享可变对象,它们必须安全地发布并且是线程安全的或由锁保护的。
安全的共享对象
在并发程序中使用和共享对象的最有用的策略是:
- 线程限制。线程限制对象由一个线程独占并限制在一个线程中,并且可以由其拥有的线程修改。
- 共享只读。一个共享的只读对象可以被多个线程同时访问而无需额外的同步,但不能被任何线程修改。共享只读对象包括不可变对象和有效不可变对象。
- 线程安全的共享。线程安全对象在内部执行同步,因此多个线程可以通过其公共接口自由访问它而无需进一步同步。
- 看守。只有持有特定的锁才能访问受保护的对象。受保护的对象包括那些封装在其他线程安全对象中的对象和已知受特定锁保护的已发布对象。