设计线程安全的类
设计一个线程安全的类要报案下面三个要素:
1. 找出构成对象的状态的所有变量。
- 对象的所有域构成了对象的状态。如果对象的域是基本变量构成,那么这些域构成了对象的全部状态。如果对象的域中引用了其他对象,那么对象的状态也包含其引用对象的域。如ArrayList的状态就包含其所有节点对象的状态。
2. 找出约束状态变量的不变性条件。
3. 建立对象状态的并发访问策略。
- 将不变性,线程封闭,加锁机制等结合起来维护线程的安全性。
先验条件(Pre-condition),后验条件(Post-condition),不变性条件(Invariant)含义如下:
- Pre-conditions are the things that must be true before a method is called. The method tells clients “this is what I expect from you”.
Post-conditions are the things that must be true after the method is complete. The method tells clients “this is what I promise to do for you”.
Invariants are the things that are always true and won’t change. The method tells clients “if this was true before you called me, I promise it’ll still be true when I’m done”.
收集同步需求
找出操作是否基于先验条件,例:取出当队列不为空。
状态的所有权
定义哪些变量将构成对象的状态时候,只考虑对象拥有的元素。
对象被哪些线程所有,哪些线程可以操作对象。
实例封闭
将数据封闭在对象内部,可以将数据的访问限制在对象方法上,从而更容易确保线程访问数据时持有正确的锁。
public class PersonSet{
private final Set<Persion> mySet = new HashSet<>();
public synchronized long addPersion(Persion p){
mySet.add(p);
}
public synchronized boolean containsPersion(Persion p){
return mySet.contains(p);
}
}
HashSet并非线程安全的,但是HashSet被封闭在PersionSet中,唯一能访问mySet的代码都由锁保护的。因此PersionSet的线程安全的。
本例中并没有假设Persion的线程安全性。如果Persion是可变的,那么访问persion还需要额外的同步。
JAVA的类库中也有很多类似的线程封闭的类,如Collections.synchronizedList及其类似方法,这些类的唯一用途就是将非线程安全的类转换成线程安全的类。
java监视器模式
java的内置锁被称为监视器锁或监视器,将所有可变对象封装起来,并有对象自己的内置锁保护。属于实例封闭。前面的Counter示例就是这种模式。也可以通过私有锁来保护对象,可以避免客户端代码获取锁,产生死锁问题,而且只需检查单个类就可以验证锁是否正确使用。
class MutablePoint{
public int x,y;
public MutablePoint(){
x=0;
y=0;
}
public MutablePoint(MutablePoint point){
this.x = point.x;
this.y = point.y;
}
}
public class MonitorVehicleTracker{
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations){
this.locations = locations;
}
public synchronized Map<String, MutablePoint> 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 IllegalStateException();
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m){
Map<String, MutablePoint> locs = new HashMap<>();
for(String id:m.keySet()){
MutablePoint loc = new MutablePoint(m.get(id));
locs.put(id, loc);
}
return Collections.unmodifiableMap(locs);
}
}
假设每辆车都有一个String对象来标记,同时拥有一个位置坐标(x,y)。通过一个线程读取位置,将其显示出来,vehicles.getLocations()
其他线程负责更新车辆的位置。vehicles.setLocation(id, x, y);
- 由于存在并发访问,必须是线程安全的,因此使用了监视器模式,确保了线程的安全。尽管MutablePoint不是线程安全的,但是可变的Point并没有被发布。当返回车辆位置时,通过deepCopy方法来复制当前的位置。因此MonitorVehicleTracker是线程安全的。通过复制可变数据类维持线程安全。可能存在一些问题,如性能问题,不能实时反映车辆位置,因为返回的是快照。
线程安全性的委托
如果类中的各个组件都是线程安全的,那么是否还需要额外的线程安全层?需要看情况。在某些情况下,通过线程安全类组合而成的类是线程安全的,称之为线程安全性的委托。
将车辆追踪器的实例改变下,代码如下:
class Point{
public final int x,y;
public Point(int x, int y){
this.x=x;
this.y=y;
}
}
public class MonitorVehicleTracker{
private final ConcurrentHashMap<String, Point> locations;
private final Map<String, Point> unModifiableMap;
public MonitorVehicleTracker(Map<String, Point> locations){
this.locations = new ConcurrentHashMap<>(locations);
unModifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, Point> getLocations(){
return unModifiableMap;
}
public void setLocation(String id, int x, int y){
if(locations.replace(id, new Point(x, y)) == null)
throw new IllegalStateException();
}
}
我们只是将最初的可变MutablePoint类变成不可变的Poient,不可变的值可以自由的分享和发布,因此返回的locattion不需要复制。使用了线程安全的ConcurrentHashMap来管理,因此没有使用显示的同步,同时确保了线程安全。将线程安全委托给ConcurrentHashMap。
委托给独立的状态变量
上面我们都是委托给单个线程安全的状态变量。我们也可以委托给多个状态变量,但是这些变量必须是彼此独立的,即组合后的类在多个状态变量上没有任何不变性条件。
委托失效
大多数组合对象存在着某些不变性条件。会导致委托失效,非线程安全。
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 IllegalArgumentException();
lower.set(i);
}
public void setUpper(int i){
if(i < lower.get())
throw new IllegalArgumentException();
upper.set(i);
}
public boolean isInRange(int i){
return (i >= lower.get()) && i <= upper.get();
}
}
- NumberRange不是线程安全的,因为进行了先检查后执行操作,并且这个操作不是原子性的,破坏了上下界进行约束的不变性条件。setLower和setUper都尝试维持不变条件,但是失败了。我们可以通过加锁机制来维护不变性条件来确保线程安全性。因此类似的符合操作,仅靠委托无法实现线程安全。
- 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束,也不存在无效的状态转换,n那么就可以安全的发布这个变量。
在现有的线程安全类中添加功能
对一个线程安全的类添加原子操作,但是,这通常做不到,因为无法修改源代码。我们可以扩展这个类,例如BetterVector对Vector进行了扩展,添加一个原子方法,putIfAbsent。
public class BetterVertor<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if(absent)
add(x);
return absent;
}
}
上述示例之所以线程安全,是因为Vector将状态向子类公开,并且规范中定义了同步策略。
客户端加锁机制
我们可以用第三种方式来在线程安全类中添加功能,扩展类的功能,并不扩展类的本身,将扩展代码放入辅助类中。
public class ListHepler<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
}
这个类看起来是线程安全的,毕竟使用了同步方法。然而这并不是线程安全的,问题在于在错误的锁上进行了同步。因为不管list使用哪个锁来保护状态,但肯定不是ListHelper上的锁。意味着putIfAbsent相对于List的其他操作并不是原子的。
要想使这个方法正确执行,必须是List在实现客户端加锁时使用同一个锁。下面是正确的示例。
public class ListHepler<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x){
synchronized(list){
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
}
}
组合
public class ImprovedList<E> {
private final List<E> list;
public ImprovedList(List<E> list){
this.list = list;
}
public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
pubic sunchronized void otherMethod(){
...
}
}
客户端并不会直接使用list这个对象,因此并不关心list是否是线程安全的,ImprovedList通过自身内置锁增加了一层额外的锁。事实上,我们使用了监视器模式封装了现有的list。只要确保客户端代码不直接使用list就能确保线程安全性。
https://www.cnblogs.com/lilinwei340/p/6942317.html