Java并发 - 线程安全的设计方式

2021/08/23 Concurrent 共 2241 字,约 7 分钟
Bob.Zhu

本文从面向对象设计的角度出发介绍几种保障线程安全的常用技术。这些技术的使用通常可以使得在不必 借助锁的情况下保障线程安全,从而既避免锁可能导致的问题以及开销,又有利于提高系统的并发性并 简化代码。另外,本文还介绍了常用的线程安全的集合对象。

无状态对象

对象(Object)就是操作和数据的封装。对象所包含的数据就被称为该对象的状态(State),它包括存储在 实例变量或者静态变量之中的数据。一个对象的状态也可能包含该对象引用的其他对象的实例变量或者 静态变量中的数据。相应地,实例变量、静态变量也被称为状态变量(StateVariable)。如果一个类的 同一个实例被多个线程共享并不会使这些线程存在共享状态( Shared State), 那么这个类及其任意 一个实例就被称为无状态对象( Stateless Object)。 反之,如果一个类的同一个实例被多个线程共享, 会使这些线程存在共享状态,那么这个类及其任意一个实例就被称为有状态对象(StatefulObject)。 无状态对象不含任何实例变量,且不包含任何静态变量或者其包含的静态变量都是只读的(常量)。 有状态对象又可以分为状态可变对象和状态不可变对象。所谓状态可变就是,对象在其生命周期中, 其状态变量的值可以发生变化。

无状态对象具有线程安全性,这有两层含义:

  • 首先,无状态对象的客户端代码在调用该对象的任何方法时都无须加锁。
  • 其次,无状态对象自身的方法实现也无须使用锁。

无状态对象可以被多个线程共享,而其客户端代码及其自身的方法实现又无须使用锁,从而避免了锁可能 产生的问题(例如死锁)以及开销。因此,无状态对象有利于提高并发性。然而,有时候设计出一个纯粹的 无状态对象可能有些难度。另外,即便是纯粹的无状态对象,随着代码的维护,它也可能逐渐演变成 其内部实现需要借助锁等线程同步机制的”非纯粹”的无状态对象:无状态对象的一些方法可能在代码维护 过程中需要访问一些非线程安全对象,而这些对象的访问可能导致这些方法的执行线程存在共享状态。

正确编写Servlet类

无状态对象的一个典型应用就是Java EE中的Servlet。Servlet是一个实现javax.servlet.Servlet 接口的托管( Managed)类,而不是一个普通的类。所谓托管类,是指Servlet 类实例的创建、初始化 以及销毁的整个对象生命周期完全是由JavaWeb服务器(例如 Tomcat)控制的,而服务器为每一个 Servlet 类最多只生成-一个实例,该唯一实例会被用于处理服务器接收到的多个请求。即一个Servlet 类的一个 (唯一的)实例会被多个线程共享,并且服务器调用Servlet.service 方法时并没有加锁,因此使Servlet 实例成为无状态对象有利于提高服务器的并发性。这也是Servlet类一般不包含实例变量或者静态变量的 原因:一旦Servlet类包含实例变量或者静态变量,我们就需要考虑是否使用锁以保障其线程安全。

不可变对象

不可变对象( Immutable Object) 是指- -经创建其状态就保持不变的对象。不可变对象也具有固有的 线程安全性,因此不可变对象也可以像无状态对象那样被多个线程共享,而这些线程访问这些共享对象的时候 无须加锁。当不可变对象所建模的现实实体的状态发生变化时,系统通过创建新的不可变对象实例来进行反映。 一个严格意义上的不可变对象要同时满足以下所有条件:

  • 类本身使用final修饰:
    这是为了防止通过创建子类来改变其定义的行为。
  • 所有字段都是用final 修饰的:
    使用final 修饰不仅仅是从语义上说明被修饰字段的值不可改变;更重要的是这个语义在多线程环境下 保证了被修饰字段的初始化安全,即final修饰的字段在对其他线程可见时,它必定是初始化完成的。
  • 对象在此初始化过程中没有逸出( Escape)
    防止其他类(如该类的内部匿名类)在对象初始化过程中修改其状态。
  • 任何字段, 若其引用了其他状态可变的对象(如集合、数组等),则这些字段必须是private修饰的, 并且这些字段值不能对外暴露。若有相关方法要返回这些字段值,则应该进行防御性复制(DefensiveCopy)。

线程特有对象

如果多个线程需要共享同一个非线程安全对象,那么我们往往需要借助锁来保障线程安全。事实上,我们 也可以选择不共享非线程安全对象一对于- 一个非线程安全对象,每个线程都创建一个该对象的实例, 各个线程仅访问各自创建的实例,且一个线程不能访问另外一个线程创建的实例。这种各个线程创建 各自的实例,-一个实例只能被一个线程访问的对象就被称为线程特有对象(TSO, Thread Specific Object ),相对应的线程就被称为该线程特有对象的持有线程。线程特有对象既保障了对非线程安全对象的 访问的线程安全,又避免了锁的开销。

ThreadLocal类相当于线程访问其线程特有对象的代理( Proxy ),即各个线程通过这个对象可以创建 并访问各自的线程特有对象,其类型参数T指定了相应线程特有对象的类型。一个线程可以使用不同的 ThreadLocal实例来创建并访问其不同的线程特有对象。多个线程使用同一个ThreadLocal实例 所访问到的对象是类型T的不同实例,即这些线程各自的线程特有对象实例。因此,ThreadLocal类也可以 理解为当前线程访问其线程;特有对象的代理对象,这种代理与被代理的关系如图6-2所示。

ThreadLocal

参考资料

文档信息

Search

    Table of Contents