到目前为止,我们已经介绍了关于线程安全与同步的一些基础知识。然而,我们并不希望对每一系内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。

4.1  设计线程安全的类

  通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。

  在设计线程安全类的过程中,需要包含以下三个基本要素:
  • 找出构成对象状态的所有变量。
  • 找出约束状态变量的不变性条件。
  • 建立对象状态的并发访问管理策略。

  要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。

  看如下清单:使用Java 监视器模式的线程安全计数器

public class Counter {
private long value = ;
public synchronized long getValue() {
return value;
}
public synchronized long increment() {
if (value == Long.MAX_VALUE) {
throw new IllegalArgumentException("");
}
return ++value;
}
}

  同步策略(Synchronization Policy)定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了那些变量由那些锁来保护。

4.1.1  收集同步需求

  要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。同样,在操作中还会包含一些后验条件来判断状态迁移是否有效的。如自增值。

  由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得更高的灵活性或性能。

如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。

4.1.2  依赖状态的操作

  类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖的操作。

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

4.2  实例封装

  如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

  封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(instance Confienement)。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。

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

  程序清单:通过封装机制来确保线程安全

public class PersonSet {
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containPerson(Person p) {
return mySet.contains(p);
}
}

  实例封装是构建线程安全类的一个最简单方式,它还使得在锁策略的选择上拥有了更多的灵活性。

  当然,如果将一个本该本封闭的对象发布出去,那么也会破坏封闭性。如果一个对象本应该封闭在特定的作用域内,那么让该对象逸出作用域就是一个作物。当发布其他对象时,例如迭代器或内部的类实例,可能会间接地发布被封闭的对象,同样会使本封闭的对象逸出。

封闭机制更容易构造线程安全的类,因为当类封闭的状态时,在分析类的线程安全性时就无须检查整个程序。

4.2.1  Java监视器模式

  从线程封闭原则及其逻辑推理可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

  程序清单:通过一个私有锁来保护状态

public class PrivateLock {
private final Object myLock = new Object();
void someMethod() {
synchronized (myLock) {
//do something
}
}
}

  使用私有的锁对象而不是对象的内置锁(任何其他可通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,是客户代码无法得到锁,但客户代码可以通过公有方法来访问,以便参与到它的同步策略中。如果客户代码错误地获得了另一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有访问的锁在程序中是否被正确地使用,则需要检查整个程序,而不是单个的类。

4.2.2  示例:车辆追踪

  以下程序清单中,我们看一个示例: 一个用于调度车辆的“车辆追踪器”。首先使用监视器模式来构建车辆追踪器,然后尝试放宽某些封装性需求同时又保持线程安全性。

public class MonitorVehicleTracker {
private final Map<String ,MutablePoint> locations;
public MonitorVehicleTracker(Map<String ,MutablePoint> locations) {
this.locations = deepCopy(locations); //返回拷贝信息
} public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations); //返回拷贝信息
} public synchronized MutablePoint getLocation(String id) {
MutablePoint lo = locations.get(id);
return lo == null ? null : new MutablePoint(lo); //返回拷贝信息
} public synchronized void setLocations(String id, int x, int y) {
MutablePoint lo = locations.get(id);
if (lo == null) {
throw new IllegalArgumentException("");
}
lo.x = x;
lo.y = y;
} private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> locations) {
Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
for (String id : locations.keySet()) {
result.put(id, new MutablePoint(locations.get(id)));
}
return Collections.unmodifiableMap(result);
}
}
public class MutablePoint {        【不要这么做】
public int x, y;
public MutablePoint() {
x = 0; y = 0;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}

  虽然类 MutablePoint 不是线程安全的,但追踪器类时线程安全的。它所包含的 Map 对象和可变的 Point 对象都未曾发布。当需要返回车辆的位置时,通过 MutablePoint 拷贝构造函数或者 deepCopy 方法来复制正确的值,从而生成一个新的Map 对象,并且该对象中的值与原有 Map 对象中的 key 值和 value 值都相同。

  在某种程度上,这种实现方式是通过再返回客户代码之前复制可变的数据来维持线程安全性的。通常情况下,这并不存在性能问题,但在车辆容器非常大的情况下将极大地降低性能。

4.3  线程安全性的委托

4.3.1  示例:基于委托的车辆追踪器

  下面将介绍一个更实际的委托示例,构造一个委托给线程安全类的车辆追踪器。我们将车辆位置保存到一个 实现线程安全的Map 对象中,还可以用一个不可变的 Point 类来代替 MutablePoint 以保存位置。

  程序清单: 在DelegatingVehicleTracker 中使用的不可变 Point 类

public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

  由于Point 类时不可变的,因而它是线程安全的。  将线程安全委托给 ConcurrentHashMap。

public class DelegatingVehicleTrack {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTrack(Map<String, Point> pointMap) {
locations = new ConcurrentHashMap<String, Point>(pointMap);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocations(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null) {
throw new IllegalArgumentException("");
}
}
}

  在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置图。

4.3.2  独立的状态变量

  到目前为止,这些委托示例都仅仅委托给了单个线程安全的状态变量。我们还可以将线程安全性委托给多个状态变量,只要这些变量时彼此独立的,即组合而成的类并不会再其包含的多个状态变量上增加任何不变性条件。

  程序清单:将线程安全性委托给多个状态变量

public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
public void addKeyListener(KeyListener keyListener) {
keyListeners.add(keyListener);
}
public void addMouseListener(MouseListener mouseListener) {
mouseListeners.add(mouseListener);
}
public void removeKeyListener(KeyListener keyListener) {
keyListeners.remove(keyListener);
}
public void removeMouseListener(MouseListener mouseListener) {
mouseListeners.remove(mouseListener);
}
}

  VisualComponent 使用 CopyOnWriteArrayList 来保存各个监听器列表。它是一个线程安全的链表,特别适用于管理监听器列表。

4.3.3  当委托失败时

  大多数组合对象都不会像 VisualComponent 这样简单:在它们的状态变量之间存在着某些不变性条件。

  程序清单:NumbeRange 类并不足以保护它的不变性条件

public class NumberRange {        【不要这样做】
//不变性条件 : lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
if (i > upper.get()) {  //  不安全的 先检查后执行
System.out.println("lower > upper");
return;
}
lower.set(i);
}
public void setUpper(int i) {
if (i < lower.get()) {  //  不安全的 先检查后执行
System.out.println("lower > upper");
return;
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}

  NumberRange 不是线程安全的,没有维持对下界和上界进行约束的不变性条件。假设取值范围在(0, 10),如果一个线程调用 setLower(5),而另一个线程调用 setUpper(4),那么在一些错误的执行时序中,这两个调用都通过了检查,并且都设置成功。因此,虽然 AtomicInteger 是线程安全的,但经过组合得到的类却不是线程安全的。

如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。

4.3.4  发布底层的状态变量

  当线程安全性委托给某个对象的底层状态变量时,在什么条件下才可以发布这些变量从而使其他类能修改它们? 答案仍然取决于在类中对这些变量施加了那些不变性条件。

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

4.3.5  示例:发布状态的车辆追踪器

  我们来构造车辆追踪器的另一个版本,并在这个版本中发布底层的可变状态。我们需要修接口以适应这种变化,即使用可变且线程安全的 Point 类。

  程序清单:线程安全且可变的 Point 类

public class SafePoint {
private int x, y;
public SafePoint(SafePoint sp) {
this.x = sp.x;
this.y = sp.y;
}
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
public synchronized int[] get() {
return new int[] {x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}

  程序清单:安全发布底层状态的车辆追踪器

public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
public SafePoint getLocations(String id) {
return locations.get(id);
}
public void setLocations(String id, int x, int y) {
if (!locations.containsKey(id)) {
throw new IllegalArgumentException("");
}
locations.get(id).set(x, y);
}
}

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

  Java 类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类:重用能降低开发工作量、开发风险以及维护成本。有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类智能支持大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新操作。

  程序清单:扩展 Vector 并增加一个“若没有则添加”方法

public class BetterVector<E> extends Vector {
public synchronized boolean putIfAbsent(E e) {
boolean absent = !contains(e);
if (absent) add(e);
return absent;
}
}

  “扩展”方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。

4.4.1  客户端加锁机制

  看一个错误例子:非线程安全的“若没有则添加”

public class ListHelper<E> {        【不要这样做】
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E e) {
boolean absent = !list.contains(e);
if (absent) list.add(e);
return absent;
}
}

  为什么这种方式不能实现线程安全性?毕竟,putIfAbsent 已经声明为 synchronized 类型的变量,对不对?问题在于在错误的锁上进行了同步。无论List 使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是 ListHelper 上的锁。

  要想使这个方法能正确执行,必须使List 在实现客户端加锁或外部加锁时使用同一个锁。

  程序清单:通过客户端加锁来实现“若没有则添加”

public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E e) {
synchronized (list) {
boolean absent = !list.contains(e);
if (absent) list.add(e);
return absent;
}
}
}

4.4.2  组合

  当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)。看如下程序清单:通过组合实现“若没有则添加”

public class ImprovedList<E> {
private final List<E> list;
public ImprovedList(List<E> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(E e) {
boolean absent = !list.contains(e);
if (absent) list.add(e);
return absent;
}
public synchronized void clear() {
list.clear();
}
// 按照类似的方式委托List的其他方法
}

  ImprovedList 通过自身的内置锁增加了一层额外的加锁。

4.5  将同步策略文档化

  在维护线程安全性时,文档是最强大的(同时也是最未充分利用的)工具之一。

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

【Java并发.4】对象的组合的更多相关文章

  1. 《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象

    Java Concurrency in Practice,一本完美的Java并发参考手册. 查看豆瓣读书 推荐:InfoQ迷你书<Java并发编程的艺术> 第一章 介绍 线程的优势:充分利 ...

  2. [Java 并发] Java并发编程实践 思维导图 - 第四章 对象的组合

    依据<Java并发编程实践>一书整理的思维导图. 第一部分: 第二部分:

  3. Java 并发编程(四):如何保证对象的线程安全性

    01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...

  4. Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  5. 《Java并发编程实战》第三章 对象的共享 读书笔记

    一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见 ...

  6. 【Java并发编程一】线程安全和共享对象

    一.什么是线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的 ...

  7. Java并发编程(五):Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

  8. java并发程序和共享对象实用策略

    java并发程序和共享对象实用策略 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括: 线程封闭 只读共享.共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它.共享的只读对象包括不 ...

  9. JAVA并发-对象方法wait

    最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提供一个稳定的基础,Object作为java中所有对象的基类,其存在的价值不言而喻,其中wait和notify方法的实现多线程协作提供了保证. ...

  10. java并发编程笔记(四)——安全发布对象

    java并发编程笔记(四)--安全发布对象 发布对象 使一个对象能够被当前范围之外的代码所使用 对象逸出 一种错误的发布.当一个对象还没构造完成时,就使它被其他线程所见 不安全的发布对象 某一个类的构 ...

随机推荐

  1. java垃圾回收机制GC

    记得第一次总结java 的GC的时候,是刚开始在课堂上学习GC的时候,那时候许老师第一节java课 课后老师说同学们可以去深入理解一下java的GC机制: 但是是花费了三四个小时,翻看了<Thi ...

  2. How to monitor tempdb in MS SQL

    Error: tempdb is full due to active_transaction. select ss.[host_name], ss.login_name, ss.original_l ...

  3. 孟岩:通证(token)和通证经济的目的在于改善现有经济的效率性

    孟岩是最早将token翻译成为通证的区块链大咖,这个翻译已经得到到了越来越人的认可.原来它叫代币,孟岩建议把它翻译成通证.以下是孟岩关于通证的注解. (孟岩,柏链道捷CEO,CSDN副总裁,区块链通证 ...

  4. innerHTML的使用

        inerHTML是html标签的属性,成对出现的标签大多数都有这个属性,用来设置或获取位于对象起始和结束标签 内的HTML.(获取HTML当前标签的起始和结束里面的内容)不包括标签本身.   ...

  5. MVC+EF 序列化类型为“System.Data.Entity.DynamicProxies.__的对象时检测到循环引用

    用MVC+EF做简单查询时,返回json格式数据出现问题 原代码: public ActionResult JSon({ NorthwindEntities db = new NorthwindEnt ...

  6. 05.Python网络爬虫之三种数据解析方式

    引入 回顾requests实现数据爬取的流程 指定url 基于requests模块发起请求 获取响应对象中的数据 进行持久化存储 其实,在上述流程中还需要较为重要的一步,就是在持久化存储之前需要进行指 ...

  7. 17.基于scrapy-redis两种形式的分布式爬虫

    redis分布式部署 1.scrapy框架是否可以自己实现分布式? - 不可以.原因有二. 其一:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls ...

  8. python 守护进程、同步锁、信号量、事件、进程通信Queue

    一.守护进程 1.主进程创建守护进程 其一:守护进程会在主进程代码执行结束后就终止 其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes ...

  9. 查看linux中的TCP连接数

    一.查看哪些IP连接本机 netstat -an 二.查看TCP连接数 1)统计80端口连接数netstat -nat|grep -i "80"|wc -l 2)统计httpd协议 ...

  10. PostgreSQL 数据库NULL值的默认排序行为与查询、索引定义规范 - nulls first\last, asc\desc

    背景 在数据库中NULL值是指UNKNOWN的值,不存储任何值,在排序时,它排在有值的行前面还是后面通过语法来指定. 例如 -- 表示null排在有值行的前面 select * from tbl or ...