本文共 6194 字,大约阅读时间需要 20 分钟。
思维导图:
引言:
在前两篇文章中,我们介绍了如何判断一个类是否是线程安全的,也介绍了如何判断一个变量是否可以被其他线程使用,即可见性。以上两篇文章是构造线程安全类的基础。在此基础之上,我们将这在这篇文章中,初步地介绍如何从无到有或者利用已有的线程安全类的资源构造我们自己的线程安全的类。按照惯例,文章大体分成两部分:
- 理论部分:我称之为同步要素分析,即分析当前组合类所拥有的的对象(状态),以及他们之间互相约束的条件。
- 使用部分:我称之为同步管理策略,即在分析了当前的状态和约束条件后,选择不同的方式构建所需要的线程安全类。
一.同步要素分析
构造一个线程安全的类,我们需要对类中的两类要素进行分析,状态和状态的约束条件
1.1 构成对象状态的变量
对于一个类来说,域变量是决定其是否线程安全的要素之一。但是,域变量和域变量所包含的域变量(递归成引用树)却不全都是当前类的状态。那么,什么样的域变量才属于类的状态呢?
若当前类的所有域变量都是基本类型,那么所有的基本类型变量都是类的状态。
若当前类含有对象引用,而此引用所含有的部分域变量的控制权不在当前类中(即未发布到当前类)时,那么,此引用对象的这部分域变量不属于当前类的状态,如果当前类可以控制引用对象的域变量则属于当前类的状态。
1.2 约束变量的条件
要构造一个线程安全的类,必不可少的考虑条件就是状态之间的约束关系。一般来说,我们可以从以下几点分析状态间的约束关系:
- 不变性条件:即状态和状态之间存在逻辑联系,例如x是个自然数,y[] 则是x因式分解后的数构成的数组,那么x和y[]之间是 存在不变性条件的。
- 后验性条件:即状态的改变需要由以前的条件决定,典型的例子是x = x + 1,新x是由老x的值决定的。
- 状态性条件:即状态的改变与否由另一个状态的状态决定,例如进行非空判断后再执行部分代码。
二.同步管理策略
在分析了上述的两大要素后,我们根据不同的情况采用不同的手段来构建一个线程安全的类。
2.1 监视器模式
当我们想从无到有或者将手头上非线程安全的类转化为一个线程安全的类时,监视器模式是非常适合的。其核心就是将我们需要的对象封闭到一个用于修饰的类中(即只有此类可以访问这些对象),然后所有访问这些对象的手段都必须经过此修饰类进行同步处理,这种方法称之为实例封闭。
例如在下列代码中,我们希望实现一个类,用于记录所有车辆及其位置信息。所以,此类会被两种线程所访问,一是获取车辆信息的线程,可能会 用于生成车辆的分布信息。另一种就是需要更新车辆位置的线程。其核心数据结构是一个非线程安全的Map,但是经过外部类的访问控制后,就可以变为线程安全的类。
其中 MutablePoint 用于储存车辆的位置信息,这是一个不安全的类。Map也不安全,但使用实例封闭就可以构成一个线程安全的类。
@ThreadSafe public class MonitorVehicleTracker { @GuardedBy("this") private final Maplocations; public MonitorVehicleTracker(Map locations) { this.locations = deepCopy(locations); } public synchronized Map getLocations() { return deepCopy(locations); } public synchronized MutablePoint getLocation(String id) { MutablePoint loc = locations.get(id); return loc == null ? null : new MutablePoint(loc); } public synchronized void setLocation(String id, int x, int y) { MutablePoint loc = locations.get(id); if (loc == null) { throw new IllegalArgumentException("No such ID: " + id); } loc.x = x; loc.y = y; } private static Map deepCopy(Map m) { //获取当前车辆位置的快照 Map result = new HashMap (); for (String id : m.keySet()){ result.put(id, new MutablePoint(m.get(id))); } return Collections.unmodifiableMap(result); }}@NotThreadSafeclass MutablePoint { public int x, y; public MutablePoint() { x = 0; y = 0; } public MutablePoint(MutablePoint p) { this.x = p.x; this.y = p.y; }}
2.2 安全委托
将类的线程安全性委托给别的线程安全类称之为安全委托。新的类需要一个线程安全类或者在此线程安全类上进行某种改动,也可能需要多个线程安全的类才可以实现其功能。我们需要根据不同的情况进行不同的处理。
2.2.1 委托给一个线程安全的类
当我们所需要的功能完全可以由一个已存在的线程安全的类提供的话,那只需要进行简单的组合即可。如下代码所示:依旧是管理车辆的位置,线程安全性完全由ConcurrentMap保证,当前类不需要做额外的显示同步
@ThreadSafepublic class DelegatingVehicleTracker { private final ConcurrentMaplocations; private final Map unmodifiableMap; public DelegatingVehicleTracker(Map points) { locations = new ConcurrentHashMap (points); unmodifiableMap = Collections.unmodifiableMap(locations); } public Map getLocations() { return unmodifiableMap; } public Point getLocation(String id) { return locations.get(id); } public void setLocation(String id, int x, int y) { if (locations.replace(id, new Point(x, y)) == null) throw new IllegalArgumentException("invalid vehicle name: " + id); }}
2.2.2 扩展线程安全的类
在大多数情况先下,我们所需要的功能都是在一个已存在的线程安全类基础之上进行增添。我们可以选择进行继承重写或者修改其源代码,但是这种手段容易破坏原有的线程安全类的不变性条件,所以不推荐使用。
相应的,我们可以构建一个新的客户端,由客户端实现新的功能并控制同步。例如:我们当前需要在一个List中实现若没有则添加的功能,请注意,此时加锁需要加在线程安全的List上,而不是新的客户端类上,否则这个若没有则添加的功能对list的其他功能来说并不是原子的和可见的。如下列代码所示:
@NotThreadSafeclass BadListHelper{ public List list = Collections.synchronizedList(new ArrayList ()); //注意!!!这样加锁是错误的,因为这个锁是BadListHelper的内置锁,而其他线程所获得的是list的锁。 public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent){ list.add(x); } return absent; }}@ThreadSafeclass GoodListHelper { public List list = Collections.synchronizedList(new ArrayList ()); public boolean putIfAbsent(E x) { synchronized (list) { boolean absent = !list.contains(x); if (absent) { list.add(x); } return absent; } }}
2.2.3 委托给两个独立的线程安全类
当我们所需要的功能必须由两个状态实现,而且这两个状态都可以有线程安全类提供且互相独立的话,也就是说这两个状态变量之间不存在不变性条件的话,那么可以直接的将线程安全性委托给这两个独立的状态变量,而不需要做特殊的同步性处理:例如,我们需要检测鼠标和键盘的输入事件,这两者是完全独立的,所以,我们完全可以用两个线程安全的list储存这两种监听事件而不导致冲突的发生。代码如下所示:
public class VisualComponent { private final ListkeyListeners = new CopyOnWriteArrayList (); private final List mouseListeners = new CopyOnWriteArrayList (); public void addKeyListener(KeyListener listener) { keyListeners.add(listener); } public void addMouseListener(MouseListener listener) { mouseListeners.add(listener); } public void removeKeyListener(KeyListener listener) { keyListeners.remove(listener); } public void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener); }}
2.2.4 多个状态的组合使用
当新的类需要多个线程安全类作为域变量且这些域变量之间存在约束的话,就不能简单的把线程安全性委托给线程安全类,而是需要我们自己做同步处理。
下列代码不是线程安全的,他由两个线程安全的域变量构成,这两个状态的不变性条件是一个比另一个大。直接组合他们并不能使新的类获得线程安全性。比如,当前为(0,10)有两个线程同时调用了setLower(5)和setUpper(4),在特殊的执行时序下,他们都将通过检查,结果就成了(5,4),破坏了类的不变性条件。如下代码所示:
public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Warning -- unsafe check-then-act if (i > upper.get()) throw new IllegalArgumentException("can't set lower to " + i + " > upper"); lower.set(i); } public void setUpper(int i) { // Warning -- unsafe check-then-act if (i < lower.get()) throw new IllegalArgumentException("can't set upper to " + i + " < lower"); upper.set(i); } public boolean isInRange(int i) { return (i >= lower.get() && i <= upper.get()); }}
所以,多个状态变量时,需要考虑状态变量的线程安全性以及状态间的约束条件的安全性,并进行相应的同步处理。
最后,一个类是否是线程安全的应该在文档中进行维护,更进一步,可以维护更详细的同步策略。如果遇见一个没有声明是否是线程安全的类,那么,就不要假设他是线程安全的。
转载地址:https://blog.csdn.net/zh328271057/article/details/96899950 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!