理解 Java 对象的安全组合模式 – Java之音
Loading
0

理解 Java 对象的安全组合模式

前言

       一些组合模式,能够使一个类更容易成为线程安全类,并且在维护时不会无意中破坏类的安全性保证。

设计线程安全的类

设计线程安全类的三个基本要素:

       - 找出构成对象状态的所有变量;

- 找出约束状态变量的不变性条件;

- 建立对象状态的并发访问管理策略;

分析对象的状态,先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。

对于含有 n 个基本类型域的对象,其状态就是这些域构成的 n 元组,例如,二维点的状态就是它的坐标值(x,y)。

       如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域,例如,LinkedList 的状态就包括该链表中所有节点对象的状态。

依赖状态的操作:

        在某些对象的方法中还包括一些基于状态的先验条件,例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于 "非空的" 状态,如果在某个操作中包含有基于状态的先验条件,那么此操作称为依赖状态的操作。

       在单线程程序中,如果某个操作无法满足先验条件,那么只能失败。但在多线程环境中,先验条件可能会由于其他线程执行的操作而变成真,而且要一直等到先验条件为真再执行该操作。

       在 Java 中等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密关联,要想正确使用并不容易。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有的类库中的类(例如阻塞队列 Blocking Queue 或者 信号量 Semaphore)来实现依赖状态的行为。

状态的所有权:

       如果分配并填充了一个 HashMap 对象,就相当于创建多个对象:HashMap 对象,在 HashMap 对象中包含的多个对象,以及在 Map.Entry 中可能包含的内部对象,HashMap 的逻辑状态包括所有的 Map.Entry 对象以及内部对象,即使这些对象都是一些对立的对象。

       在 C++ 中,当把一个对象传递给某个方法时,需要考虑这种操作是否传递对象的所有权,是短期的还是长期的所有权,在 Java 中同样存在所有权模型,只不过垃圾回收器为我们减少了许多在引用共享方面常见的错误,降低了在所有权处理上的开销。

       所有权与封装性总是相互关联:对象封装它拥有的状态,对它封装的状态拥有所有权。所有权意味着控制权,如果发布了某个可变对象的引用,就不再拥有独占的控制权,最多是共享控制权。对于从构造函数或从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法时被专门设计为转移传递进来的对象的所有权(例如同步容器封装器的工厂方法)。

       容器类通常表现出一种 "所有权分离" 的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。当使用保存在 ServletContext 中的对象时,需要使用同步,这些对象由应用程序拥有,Servlet 容器只是替应用程序保管它们,与所有共享对象一样必须安全地共享。

实例封闭

       将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

       被封装对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类的一个私有成员)中,或者封装在某个作用域内(例如作为一个局部变量),再或者封闭在线程内(例如在某个线程中将对象从一个方法传递另一个方法,而不是在多个线程之间共享该对象)。对象本身不会逸出——出现逸出情况的原因通常是由于开发人员在发布对象时超出了对象既定的作用域。

       在 Java 类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。类库提供了包装器工厂方法(例如 Collections.synchronizedList 及其类似方法)使得 ArrayList、HashMap 等方法可以在多线程环境中安全地使用。这些工厂方法通过 "装饰器(Decorator)" 模式将容器类封装在一个同步的包装器对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象中。只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),即对底层容器对象的所有访问必须通过包装器来进行,那么它是线程安全的。

Java 监视器模式:

       遵循 Java 监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

       进入和退出同步代码块的字节指令也称为 monitorenter 和 moniorexit,而 Java 的内置锁也称为监视器锁或监视器。

使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),可以将锁封装起来使客户代码无法得到锁,但可以通过公有方法来访问锁。

基于监视器模式的车辆追踪:

       每台车都由一个 String 对象来标识,并且拥有一个相应的位置坐标(x,y),将由一个视图线程和多个执行更新操作的线程共享。

       虽然类 MutablePoint 不是线程安全的,但追踪器类是线程安全的,它所包含的 Map 对象和可变的 Point 对象都未曾发布。当需要返回车辆位置时,通过 MutablePoint 拷贝构造函数或者 deepCopy 方法来复制正确的值,从而生成一个新的 Map 对象,并且该对象中的值与原有 Map 对象中的 key 值和 value 值都相同。deepCopy 并不只是用 unmodifiableMap 来包装 Map 的,因为这只能防止容器对象被修改,而不能防止调用者修改保存在容器中的可变对象,如果只是通过拷贝构造函数来填充 deepCopy 中的 HashMap,那么同样是不正确的,因为这样只复制了指向 Point 对象的引用,而不是 Point 对象本身。

在车辆容器非常大的情况下将极大地降低性能,由于 deepCopy 是从一个 synchronized 方法中调用,因此在执行时间较长的复制操作中,其内置锁将一直被占有,将严重降低用户界面的响应灵敏度。

       而且,由于每次调用 getLocation 就要复制数据,因此将出现一种错误情况——车辆的实际位置发生变化但返回的信息却保持不变。如果在 locations 集合号上存在的一致性需求,那么就没问题,返回一致的快照就非常重要。如果调用者需要每辆车的最新信息,那么就是缺点,因为需要非常频繁地刷新快照。

线程安全性的委托

       在某些情况下,通过多个线程安全类组合而成的类是线程安全的。

基于委托的车辆追踪器:

由于 Point 类是不可变的,因而是线程安全的,不可变的值可以被自由地共享与发布,因此在返回 locations 时不需要复制。

       所有对状态的访问都由 ConcurrentHashMap 来管理,而 Map 所有的键和值都是不可变的。

在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置的视图,这意味着如果线程 A 调用 getLocations,而线程 B 在随后修改某些点的位置,那么在返回给线程 A 的 Map 中将反映出这些变化。

       如果需要一个不发生变化的车辆视图,那么 getLocations 可以返回对 locations 这个 Map 对象的一个浅拷贝(Shallow Copy),由于 Map 的内容是不可变的,因此只需要复制 Map 的结构而不是复制它的内容。

独立的状态变量:

将线程安全性委托给多个状态变量,只要这些变量时彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。

两个变量是彼此独立的,因此该类可以将其线程安全性委托给这两个线程安全的监听器列表。

CopyOnWriteArrayList 是一个线程安全的链表,特别适用于管理监听器列表。

发布底层的状态变量:

       如果一个状态变量时线程安全的,并且没有任何不可变条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。

发布状态的车辆追踪器:

getLocation 方法返回底层 Map 对象的一个不可变副本,调用者不能增加或删除车辆,但却可以通过修改返回 Map 中的 SafePoint 值来改变车辆的位置。该类是线程安全的,但如果它在车辆位置的有效值上施加了任何约束,那么就不再是线程安全的,例如对车辆位置的变化进行判断或者当位置变化时执行一些操作。

在现有的线程安全类中添加功能

客户端加锁机制:

这种方式不能实现线程安全性,问题在于在错误的锁上进行了同步。这个锁并不是 ListHelper 上的锁,只是带来了同步的假象,尽管所有的链表操作都被声明为 synchronized,但却使用了不同的锁。

正确的做法是使 List 在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象 x 的客户端代码,使用 x 本身用于保护其状态的锁来保护代码,要使用客户端加锁,必须知道对象 x 使用哪一个锁。

在 Vector 和同步封装器类的文档中指出,它们通过使用 Vector 或封装器容器的内置锁来支持客户端加锁。

通过添加一个原子操作来扩展类是脆弱的,因为将类的加锁代码分布到多个类中,而客户端加锁却更脆弱,因为将加锁代码放到完全无关的其他类中,要特别小心。

 

组合:

      通过自身的内置锁增加一层额外的加锁。它并不关心底层的 List 是否是线程安全的,即使 List 不是线程安全的或者修改了它的加锁实现,也会提供一致的加锁机制来实现线程安全性。

       事实上,使用 Java 监视器模式来封装现有的 List,并且只要在类中拥有指向底层 List 的唯一外部引用就能保证线程安全性。

(完)

—— 微信:yuezhi806,欢迎提意见~ ——

识别二维码,关注我,每周一更:

最后编辑于:2018/7/30作者: 一个八月想偷懒的开发坑

一个八月想偷懒的开发坑

我就写写,你就看看,只谈技术,可好? 微信公众号:一个八月想偷懒的开发坑 关于我:一个暂时 无趣无畏 愤世嫉俗 自暴自弃 间歇消极 无欲无求 开发坑

暂无评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

arrow grin ! ? cool roll eek evil razz mrgreen smile oops lol mad twisted wink idea cry shock neutral sad ???

服务网站公众号,会定期推送网站优质内容,网站最新动态!

服务网站公众号,会定期推送网站优质内容,网站最新动态!