一、设计线程安全的类

尽管将所有的状态都存储在公共静态域中,仍然能写出线程安全的程序,但是比起那些经过适当封装的类来说,我们难以验证这种程序的线程安全性,也很难在修改它们的同时,保证不破坏它的线程安全性。在没有进行全局检查的情况下,封装能够保证类的线程安全性。

设计线程安全类的过程应该包括下面3个基本要素:

  • 确定对象状态是由哪些变量构成的;
  • 确定限制状态变量的不变约束;
  • 制定一个管理并发访问对象状态的策略。

同步策略(synchronization policy)定义了对象如何协调对其状态的访问,并且不会违反它的不变约束或后验条件。它规定了如何把不可变性、线程限制和锁结合起来,从而维护线程的安全性,还指明了哪些锁保护哪些变量。为了保证开发者与维护者可以分析并维护类,应该将类的同步策略写入文档。
image.png

1.1-收集同步需求

维护类的线程安全性意味着要确保在并发访问的情况下,保护它的不变约束;这需要对其状态进行判断。对象与变量拥有:一个状态空间(state space):即它们可能处于的状态的范围。状态空间越小,越容易判断它们。尽量使用final类型的域,就可以简化我们对对象的可能状态进行分析。(不可变对象是一种极限情况,它只可能处于唯一的状态。)
很多类通过不可变约束来判定某—种状态是合法的还是非法的。counter中的value域是1ong类型。1ong 的状态空间跨越了从Long.MIN_VALUE到Long.MAX_VALUE的范围,但是counter约束了value的取值:不允许是负值。
不变约束与后验条件施加在状态及状态转换上的约束,引入了额外的同步与封装的需要。如果某些状态可能是非法的,则必须封装该状态下的状态变量,否则客户代码会将对象置于非法状态。如果一个操作的过程中可能出现非法状态转换,则该操作必须是原子的。另一方面,如果类并未强制任何约束,我们就可以开放一些关于类的封装或序列化的条件,以此获得更佳的灵活性或更好的性能。

不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性。

1.2-状态依赖的操作

类的不变约束与方法的后验条件约束了对象合法的状态和合法状态转换。某些对象的方法也有基于状态的先验条件(preconditions)。例如,你无法从空队列中移除一个条目;在你删除元素前,队列必须处于“非空”状态。若一个操作存在基于状态的先验条件,则把它称为是状态依赖的( state-dependent) 。

在Java中,等待特定条件成立的内置高效机制——wait和notify———与内部锁紧密地绑定在一起,因而想正确使用它们并不容易。创建一个操作,让它在执行前必须等待先验条件为真,不如使用现有类库来提供期望的状态依赖行为更容易,比如阻塞队列(blocking queue)或信号量(semaphore),以及其他同步工具(Synchronizer),这些将在第5章讲述;创建一个状态依赖的类,要使用平台与类库提供的底层机制,这些将在第14章讲述。

1.3-状态的所有权

在定义对象状态是由哪些变量构成时,我们只考虑那些对象所拥有的数据。所有权(Ownership)并不是语言中明确具化的概念,而是类设计中的元素。如果你实例化并组成了一个HashMap,那么你就创建了多个对象:HashMap对象、大量用于实现 HashMap的 Map.Entry对象,可能还包括其他的内部对象。HashMap 的逻辑状态包括所有Map.Entry和内部对象的状态,即使它们被实现为独立的对象。

容器类通常表现出—种“所有权分离”的形式。这是指容器拥有容器框架的状态,而客户代码拥有存储在容器中的对象的状态。以servlet框架中的servletContext为例。servletContext为Servlet提供了类似于Map的对象容器服务。Servlet可以通过名称,使用setAttribute和getAttribute在servletcontext 中注册与重获应用程序对象。由于Servlet容器实现的 servletContext对象一定会被多个线程访问,因此servletContext必须是线程安全的。所以在调用setAttribute和getAttribute时,Servlet是不必同步的。但是使用存储在servletContext 中的对象时,可能必须要同步。这些对象属于应用程序,Servlet容器只是存储它们并代替应用程序保管它们。正如所有的共享对象那样,它们必须安全地被共享。为了防止多线程并发访问同一对象时所带来的干扰,这些对象应该是线程安全对象、高效不可变对象或者由锁明确保护的对象’。

二、实例限制

即使一个对象不是线程安全的,仍然有许多技术可以让它安全地用于多线程程序。比如,你可以确保它只被单一的线程访问(线程限制),也可以确保所有的访问都正确地被锁保护。

通过使用实例限制(instance confinement),封装简化了类的线程安全化工作,这通常称为“限制”。当一个对象被另一个对象封装时,所有访问被封装对象的代码路径就是全部可知的,这相比于让对象可被整个系统访问来说,更容易对代码路径进行分析。把限制与各种适当的锁策略相结合,可以确保程序以线程安全的方式使用其他非线程安全对象。

将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁。

被限制对象一定不能逸出到它的期望可用范围之外。可以把对象限制在类实例((比如私有的类成员)、语汇范围((lexical scope,比如本地变量)或线程(比如对象在线程内部从一个方法传递到另一个方法,不过前提是该对象不被跨线程共享)中。对象不会自发地逸出自己,当然——这需要程序员的努力,将它们发布在其期望的可用范围内。

清单4.2的PersonSet示范了限制与锁如何协同确保一个类的线程安全性,即使它的组件状态变量并不是线程安全的。非线程安全的Hashset管理着Personset的状态。不过由于myset是私有的,不会逸出,因此Hashset被限制在Personset中。唯一可以访问myset 的代码路径是addPerson与containsPerson,执行它们都要获得PersonSet的锁。Personset的内部锁保护了它所有的状态,因而确保了Personset是线程安全的。
image.png

2.1-Java监视器模式

线程限制原则的直接推论之一是Java监视器模式(Java monitor pattern)?。遵循Java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。

很多像vector和Hashtable这样的核心库类都使用了Java监视器模式。有时程序需要一个更加精巧的同步策略;第11章讲述了如何通过精巧的锁策略来提高可伸缩性。Java监视器模式最大的优势在于简单。Java监视器模式仅仅是一种习惯约定;任意锁对象,只要始终如一地使用,都可以用来保护对象的状态。清单4.3示范了一个使用私有锁保护状态的类。
image.png

2.2-范例:机动车追踪器( tracking fleet vehicles )

我们举一个更加实际些的例子:一个用于调度出租车、警车、货运卡车等机动车的“机动车追踪器(vehicle tracker)”。我们先使用监视器模式构建它,然后看看在维护线程安全性的前提下,如何释放一些封装性条件。

每一辆机动车都有一个String 标识,并有一个与之对应的位置(x,y)。每个VehicleTracker对象都封装了一辆已知机动车的标识(identity)和位置〈location),以使它更适合作为MVC (model-view-controller)架构GUI应用中的数据模型(data model)。视图(view)线程和多个更新线程可能会共享数据模型。视图线程会获取机动车的名称和位置,将它们显示在显示器上:
image.png
类似地,更新线程会通过从GPS 设备上获取的数据或者调度员通过GUI界面手工输入的数据,修改机动车的位置。
image.png

三、委托线程安全

几乎所有对象都是组合对象。当凭空构建一个类,或者使用非线程安全对象组装一个类时,Java监视器模式十分有用。但是如果我们的类组件已经是线程安全的呢?我们需要添加一个额外的线程安全层吗﹖答案是:“依情况而定”。

注意deepCopy不能仅仅用一个unmodifiableMap包装Map,因为这样做只保护容器不被修改,并不能防止调用者修改存储在其中的可变对象。出于同样的原因,在deepCopy通过一个拷贝构造函数( copy constructor)生成HashMap同样是不正确的,因为这样做只复制了Point的引用,而不是Point对象本身。
因为deepCopy是从synchronized类型的方法中调用,线程会在一个可能很耗时的拷贝操作期间一直占有tracker的内部锁。当追踪大量的机动车时,这会降低用户接口的响应性。
image.png
image.png

3.1-范例:使用委托的机动车追踪器

作为一个更真实的委托示例,让我们创建一个新版本的机动车追踪器,它将会委托线程安全的类。我们使用Map 存储location,所以就从一个线程安全的 Map 实现——ConcurrentHashMap——开始。我们用不可变的 Point类取代MutablePoint,来存储location信息,正如清单4.6所示。
image.png
Point类是不可变的,因而是线程安全的。程序可以自由地共享与发布不可变值,所以我们返回location时不必再复制它们。
image.png
如果我们使用原先的MutablePoint类代替Point,就会让 getLocations发布一个非线程安全的可变引用,从而会破坏封装性。请注意,我们已经略微改变了vehicleTracker类的行为;基于“监视器”的代码返回location 的快照,基于“委托”的代码返回一个不可变的,但却是“现场(live)”的 location 视图。这意味着如果线程A调用getLocations时,线程B修改了一些Point 的 location,这些变化会反映到返回给线程A的wap值中。正如我们在前面提到的,这可能是好事(获得最新的数据)也可能是坏事(车辆移动时位置的潜在不—致性),一切取决于你的需求。
如果需要一个不可变的瞬时(fleet)视图, getLocations可以返回一个location Map的灰拷贝( shallow copy,只复制对象的引用,因此复制的对象与原始的对象是同一个对象;相反,深度拷贝(deepCopy)会复制对象的所有成员,因此复制的对象与原始的对象是不同的对象。)。因为Map 的内容是不可变的,因此需要复制的只有Map的结构,而不包括的它的内容,正如清单4.8所示(它返回一个普通的HashMap,getLocations并不承诺返回一个线程安全的Map)。
image.png

3.2-非状态依赖变量

目前为止,在委托示例中仅仅委托了一个单-的线程安全的状态变量。我们也可以将线程安全委托到多个隐含的状态变量上,只要这些变量是彼此独立的,这意味着组合对象并未增加任何涉及多个状态变量的不变约束。
清单4.9的visualComponent是一个允许客户注册鼠标键盘事件监听器的图形组件。它为每种类型的事件维护一个已注册监听器的清单,因而一个事件发生时,程序会调用相应的监听器。但是鼠标事件的监听器集与键盘事件的监听器集之间没有关系:它们彼此独立,因此visualComponent可以将它的线程安全委托到这两个线程安全的清单上。
image.png

3.3-当委托无法胜任时

大多数组合对象不像visualComponent这样简单:它们的不变约束与组件的状态变量相联系。清单4.10的NumberRange使用两个AtomicInteger管理它的状态,而且受到一个额外的约束限制—-第一个数小于或等于第二个。
image.png
NumberRange不是线程安全的;它没有保护好用于约束1ower和upper的不变约束。setLower 和 setUpper试图保护不变约束,但又显得力不从心。setLower和setUpper都是“检查再运行”的操作,但是它们没有适当地加锁以保证其原子性。

3.4-发布底层的状态变量

当你将线程安全性委托给一个对象底层的状态变量时,在什么条件下你可以发布这些变量,允许其他类也修改它们?答案再一次取决于不变约束对这些变量施加了何种影响。尽管counter的value域自身允许等于任何整数值,但是counter限制它只能等于正数,同时自增操作还限制了任何给定当前状态的下一有效状态的集合。如果你将value声明为public,客户可以将它改为一个无效值,所以发布它可能导致类出现错误。另一方面,如果变量表示的是当前温度或者上一个登录用户的 ID,那么即使其类修改了这个值大概也不会破坏任何不变约束,进而发布这个变量是允许的。(这可能仍然不是一个好主意,因为发布一个可变变量会限制未来一些子类的开发,但这并不是造成类不安全的必要因素。)

3.5-示例:发布了状态的机动车追踪器

image.png
清单4.11的safePoint 提供的getter方法可以一次获得x与y的值,并返回-一个二元数组°。如果我们为x和y 分别提供getter方法,那么x 与y的值可以在获得两个不同坐标的时间之间发生改变,结果是调用者会看到一个不一致的值:位置(x,y)上从来没有任何机动车。我们可以使用safePoint构建一个机动车追踪器,它发布了底层的可变状态却不破坏线程安全性,如清单4.12的PublishingvehicleTracker类所示。
image.png

四、向已有的线程安全类添加功能