设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
找出同步需求(不变性与后验条件)
如果一个类中含有不可变对象属性,则应该保证其不变性条件在并发访问情况下被满足;
另外如果有需要更新的对象属性,则需要保证其后验条件在状态以及状态转换上施加约束。比如一数字做递增操作,如这次值为22,下一次的有效状态必将为23 (有点CAS的味道?)
它们约束了在对象上有哪些状态 (不变性以及对状态的范围约束) 和状态转移是有效的 (后验条件) 。
依赖状态的操作(先验条件)
先验条件就是执行某段代码必须先满足的条件,在单线程中若不满足之后也不会满足,而多线程可能会被其他线程改写状态,就满足条件得以执行了,一般常用的机制为等待和通知机制(阻塞队列或信号量可简单实现)。
状态的所有权(不要发布不必要的属性对象)
实例封闭(将对象封装在线程安全类内部)
若干某对象不说线程安全的,可以采用 线程封闭、加锁在多线程中线程安全的使用。将数据封装在对象内部,将数据的访问限制在方法上从而更容易确保线程在访问数据时总能持有正确的锁。
`
//mySet是不可变对象,但是HashSet本身并不是线程安全的,
//不过他的作用域被封装在在类中,且读写方式都加了锁,
//所以是线程安全的。
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);
}
}
Java监视器模式
使用私有的锁而不是对象的内置锁来保护对象的某状态。好处在于客户代码无法直接得到锁,但可以通过公有方法来访问锁,发生减少错误的使用锁时的检查范围。(书上的例子太长了懒得抄了,写个简短的吧,还是上面那个PersonSet类,假设需要一个方法可以返回现在的Set的情况,那么我们按照上诉思想应该这么写)
//简单的说就是返回的仅仅是属性对象数据的备份而不是属性对象本身,
//这样属性对象的所有权仍在它所属对象中。
//不过这种方式的缺陷是一旦数据比较庞大时,就会很慢。
public synchronized Set<Person> getSet(){
//return new HashSet<>(mySet);//这样写是不对的
//里面的存储的对象也需要深复制一下。
return new HashSet<>(set.stream(). map(Person::new)
.collect(Collectors.toList()));
}
当很多属性都是非线程安全类时,Java监视器模式还是很有用的。
线程安全性的委托
当类中组件为线程安全时是否还需要这种机制?视情况而定,比如如果该类的状态由线程安全的对象决定,而且对它们的操作还是原子操作 (原子操作=原子操作,原子操作+原子操作不一定还是原子操作) 这时便不需要额外的永不机制,反之则需要。
如果Preson类为线程安全的不可变对象,那么结合委托,我们就可以将代码这亚子写。
public class Person{
public final String id;//不可变对象,不会出现线程不安全
public Person(String id){
this.id = id;
}
}
public class PersonSet{
private final Set<Person> mySet
= new CopyOnWriteArraySet<>();//线程安全的set
private final Set<Person> unmodifiableSet =
Collections.unmodifiableSet(mySet);//将mySet包装,
//返回一个不可修改的set
public void addPerson(Person p){
mySet.add(p);
}
public boolean containsPerson(Person p){
return mySet.contains(p);
}
public Set<Person> getSet(){
//因为我们的类的状态只有mySet,
//而mySet只是用于管理Person对象的
//而Person对象是不可变的线程安全类,
//所以我们仅需要保证mySet不会逸出即可。
return unmodifiableSet;
}
}
发布底层状态变量
刚才都说的是不能发布底层状态,只能将它的数据copy出去,可是并不是所有的场景都如此,有些场景就可以将其直接发布出去,还能节省一些资源。如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在操作上也不存在不允许的状态转换,则可以安全的发布它。
//线程安全可变的Person类
public class Person{
@GuardedBy("this") private String id;
public Person(String id){
this.id = id;
}
public Person(Person p){
this.id = p.get();
}
public synchronized String get(){
return new String(id);
}
public synchronized void set(String id){
this.id = id;
}
}
//由委托的那个代码,加一个这个方法
public Person getPerson(int inx){
//虽然这么写挺没啥意义的,
//失算了...又懒得改。
//反正就那意思,因为Person满足前面说的三个条件,
//所以是可以直接发布的不需要copy。
return unmodifiableSet.stream().
map(String::valueOf).collect(Collectors.toList()).
get(inx);
}
淦太狗了,chrome崩了,复制过来重新排版太糟心了。