保障线程安全的设计技术

本文摘抄自《Java 多线程编程实战指南》核心篇 第六章小结

本章从面向对象编程的角度出发讲解了实现线程安全的几种常用技术。这些技术的运用通常可以产生具有固有线程安全性的对象,即这些对象本身无须借助锁就可以保障线程安全,从而有利于提高系统的并发性。本章还介绍了同步集合和并发集合。


  Java 运行时空间可分为堆空间/非堆空间以及栈空间。栈空间是线程的私有空间,而堆空间和非堆空间都是线程的共享空间。堆空间用于存储对象以及类的实例变量,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。非堆空间用于存储类的静态变量以及其他元数据,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。栈空间用于存储线程所执行的方法的局部变量/返回值等私有数据,它是线程创建时分配的容量固定不可变的存储空间。

  无状态对象不包含任何实例变量以及可更新的变量,它具有固有的线程安全性。无状态对象的客户端代码在调用该对象的任何方法时都无须加锁,而无状态对象自身的方法实现可能仍然需要借助锁。仅包含静态方法的类并不能取代无状态对象。Servlet 类通常需要被设计为无状态对象。

  不可变对象也具有固有的线程安全性。严格意义上的不可变对象需要同时满足这几个条件:类本身采用 final 修饰,所有字段都是 final 字段,在对象初始化过程中 this 代表的当前对象没有逸出,引用了状态可变的对象的字段不能直接暴露给其他对象。如果需要将引用了状态可变的对象的字段暴露给其他对象,那么需要在返回该对象前进行防御性复制,或者返回一个不支持 remove() 的 Iterator 实例。使用不可变对象建模时,系统状态的变化是通过创建新的不可变对象实现的。这种方式可能有利于提高垃圾回收效率,但也可能由于系统状态频繁变更/无状态对象占用较多内存空间等因素增加了垃圾回收的负担。不可变对象的典型应用场景包括:被建模对象的状态变化不频繁/同时对一组相关的数据进行写操作,因此需要保证原子性/使用不可变对象作为安全可靠的 Map 键。当被建模对象的状态变更比较频繁时,不可变对象也不见得就不能使用。此时,我们需要综合考虑被建模对象的规模/代码目标运行环境的 Java 虚拟机堆内存容量/系统对吞吐率和响应性的要求这几个因素。

  线程特有对象也具有固有的线程安全性。ThreadLocal 是线程访问其线程特有对象的代理。ThreadLocal 也被称为线程局部变量,一个线程可以通过使用不同的线程局部变量来访问不同的线程特有对象实例。多个线程即使是使用同一个线程局部变量,其访问到的对象也是各自的线程特有对象。线程局部变量通常作为一个类的静态字段来使用。为避免线程局部变量的使用导致内存泄露和伪内存泄露,我们需要确保在线程特有对象不再被需要的时候将其“删除”(即调用 ThreadLocal.remove())。线程特有对象的典型应用场景包括:需要使用非线程安全对象,但又不希望因此而引入锁;使用线程安全对象,但希望避免其使用的锁的开销和相关问题;实现方法间的隐式参数传递;实现特定于线程的单例模式。

  装饰器模式也能够用于实现线程安全。在使用装饰器模式的情况下,实现同一组功能的对象有非线程安全版和线程安全版两种。这两种对象具有相同的接口,其中非线程安全版对象仅关注功能的实现,而外包装对象(线程安全版)主要关注线程安全的保障。外包装对象在功能方面则是通过委托给相应的非线程安全对象来实现的。Java 并发集合就是使用装饰器模式来保障线程安全的。使用装饰器模式实现线程安全的优点是它支持关注点分离,并有利于降低开发难度和提高代码的可测试性,也有利于提高使用的灵活性。其缺点是并发性不高,并可能导致遍历操作是非线程安全的。

  并发集合一般可用于替代同步集合。其内部实现往往借助于 CAS 操作或者细粒度锁。并发集合支持线程安全的遍历操作,即对集合的遍历操作与更新操作是可以由不同线程并发执行的。并发集合实现线程安全的遍历操作由两种方式:快照和准实时。前者无法在遍历过程中反映其他线程对被遍历集合所做的更新,而后者在遍历过程中可能反映其他线程对被遍历集合所做的更新。CopyOnWriteArrayList 相当于 ArrayList 的线程安全版,它适用于遍历操作远比更新操作频繁或者不希望在遍历的时候加锁的场景,在其他场景下我们仍然要考虑使用相应的同步集合。CopyOnWriteArraySet 相当于 HashSet 的线程安全版,内部实现是基于 CopyOnWriteArrayList 的。因此,CopyOnWriteArraySet 适用场景与 CopyOnWriteArrayList 类似。ConcurrentLinkedQueue 相当于 LinkedList 的线程安全版,与 BlockingQueue 的实现类相比,ConcurrentLinkedQueue 适用于更新操作和遍历操作并发的场景。BlockingQueue 的实现类更适合于多个线程并发更新同一队列的场景,如生产者——消费者模式中。ConcurrentSkipListMap 相当于 TreeMap 的线程安全版。ConcurrentSkipListSet 相当于 TreeSet 的线程安全版。

本章知识结构图

文章作者: DoubleFJ
文章链接: http://putop.top/2019/08/18/Multithreading-Chapter-Six/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 DoubleFJ の Blog