作者: 康凯森
日期: 2016-11-27
分类: Java
设计线程安全类的基本要素:
如果对象的所有域都是基本类型的变量,这些变量就构成了对象的全部状态。
如果对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。
例如 List
的状态就包含列表中所有节点对象的状态。
由于不变性条件和后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。
如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。
当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。
/**
* Created by kangkaisen on 2016/11/27.
* 使用java监视器模式的线程安全计数器
*/
@ThreadSafe
public class Counter {
@GuardedBy("this")
private long value = 0;
public synchronized long getValue() {
return value;
}
public synchronized long increment() {
if (value == Long.MAX_VALUE) {
throw new IllegalStateException("counter overflow");
}
return ++value;
}
}
实例封闭是构建线程安全类的一个最简单方式。
通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
/**
* Created by kangkaisen on 2016/11/27.
* 通过封闭机制来确保线程安全
*/
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> myset = new HashSet<>();
public synchronized void addPerson(Person p) {
myset.add(p);
}
public synchronized boolean containsPerson(Person p) {
return myset.contains(p);
}
}
/**
* Created by kangkaisen on 2016/11/27.
* 通过一个私有锁来保护状态.
* 私有的锁对象可以将锁封装起来,使客户代码无法得到锁.
*/
@ThreadSafe
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock")
Person person;
void someMethod(){
synchronized (myLock) {
//访问或修改person的状态
}
}
}
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有操作中都不包含无效的状态转换,就可以将线程安全性委托给底层的状态变量。(AtomicLong
,ConcurrentHashMap
等)
如果某个类有复合操作,例如NumberRange
,那么仅靠委托并不足以实现线程安全性。此时,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
/**
* Created by kangkaisen on 2016/11/27.
* Bad case!
* NumberRange 类并不足以保护它的不变性条件
*/
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()) {
throw new IllegalStateException("can't set lower, because it > upper");
}
lower.set(i);
}
public void setUpper(int i) {
// 不安全的 "先检查后执行"
if (i < lower.get()) {
throw new IllegalStateException("can't set upper, because it < lower");
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
/**
* Created by kangkaisen on 2016/11/27.
* 线程安全且可变
*/
@ThreadSafe
public class SafePoint {
@GuardedBy("this")
private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
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;
}
}
重用现有的类可以降低开发工作量,开发风险以及维护成本。
在现有的线程安全类中添加功能有以下方法:
客户端加锁是指:对于使用某个对象X的客户端代码,使用X本身保护其自身状态的锁在客户端保护这段代码。 所以必须知道对象X使用的是哪个锁。
通过组合实现在现有的线程安全类中添加功能的代码示例:
/**
* Created by kangkaisen on 2016/11/27.
* 通过组合实现"若没有则添加"
*/
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (!contains) {
list.add(x);
}
return contains;
}
public synchronized void clear() {
list.clear();
}
//按照类似的方式委托list的其他方法.
}
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
如果某个类没有声明是线程安全的,就不要假设它是线程安全的。
本文是《Java并发编程实战》的读书笔记。