编写线程安全的代码时,我们不希望对每次内存访问都进行分析一确保程序是线程安全的,而是希望将一些现有的安全组建组合为更大规模的组建或程序。下面介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的,并且在维护这些类是不会无意中破坏类的安全性保证的。
设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
对象的状态是由对象的域组成的,有0-n个不等,如果域都是基本类型,那这些域构成对象的全部状态,如果有引用类型,那么该对象的状态包括被引用对象的域(如LinkedList的状态包括链表中所有节点对象的状态)。
实例封装
如果对象不是线程安全的额,那么可以通过多种技术使他在多线程程序中安全的使用。
- 可以确保该对象只能由单个线程访问(线程封闭),如JDBC Connection对对象,ThreadLocal。
- 通过一个锁来保护该对象的所有访问(Java监视器模式),如PersonSet,代码在后面。
- 封装对象,只暴露可访问的方法。与对象由整个程序访问的情况比,更容易对代码进行分析。如Collections中的UnmodifiableCollection。
// mySet不会逸出,唯一的外部引用就是PersonSet,使用Java监视器来封装能确保线程安全。
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程的访问数据室总能持有正确的锁。
线程安全性的委托
我们可以把线程安全委托给先有的线程安全类,这样我们的代码阿九不用关心线程安全的问题了额。这里有两种情况:
- 如果委托给单独的线程安全类,能保证线程安全。如,我们可以使用ConcurrentHashMap保存线程共享数据。
- 如果委托给两个或两个已上的线程安全类,如果存在竞态条件,需要额外的同步机制保证;如果分别表示独立的状态,可以不使用额外的同步机制即可保证线程安全。
- 在现有线程安全类中添加功能,叫作客户端加锁。这种机制是派生类的行为与基类耦合在一起,破坏了基类的同步策略,使用时需要特别小心。
示例代码:
// 情况2,需要增加同步机制保证 check-than-act
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 , need a lock
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 , need a lock
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());
}
}
小结
这一章介绍了实现线程安全类是采用的一些技术。
- 线程安全可以委托给现有的线程安全类。
- 委托是创建线程安全的一个有效策略。
- 值需要让现有的线程安全类管理所有的状态即可
- 当需要使用多个线程安全类保存状态是,需要额外的同步机制保证。
网友评论