《JAVA并发编程实战》对象的组合

版权声明:本文为博主原创文章,转载请在明显的位置注明出处。如果觉得文章对你有用,给我点个赞呗~ https://blog.csdn.net/sayWhat_sayHello/article/details/83314281

设计线程安全的类

在设计线程安全类的过程中,需要包含以下三个基本要素:

  1. 找出构成对象状态的所有变量
  2. 找出约束状态变量的不变性条件
  3. 建立对象状态的并发访问管理策略

找出构成对象状态的所有变量

分析对象的状态,首先从对象的域开始:

  • 如果对象所有域都是基本类型的变量,那么这些域构成对象的全部状态。对于含有n个基本类型域的对象,其状态就是这些域构成的n元组。
  • 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。

示例

public class Person{
    private int id;
}

这个对象的域为id,这个域就是Person对象的全部状态。

public class Coordinate{
    private int x;
    private int y;
}

这个对象有两个域x,y;Coordinate对象的状态为二元组(x,y);

public class Diary{
    private Person person;
}

这个对象有两个域person,这个域都是引用类型。所以状态包含person对象里的域id.

找出约束状态变量的不变性条件

利用不变性条件判断状态是否有效,例如整型int类型,状态空间为Integer.MIN_VALUE~Integer.MAX_VALUE

后验条件判断迁移是否有效。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。

实例封闭

如果某对象不是线程安全的,一般可以通过两种技术使其在多线程程序中安全的使用:

  • 确保该对象只能由单个线程访问(线程封闭)
  • 通过一个锁来保护该对象的所有访问。
public class PersonSet{
    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);
    }
}

这个类是线程安全的,虽然HashSet是非线程安全的,但是mySet是私有的,且不会被get,因此HashSet是被封闭在PersonSet中的。而且能访问mySet的addPerson和containsPerson在执行时需要访问PersonSet的内置锁,因此PersonSet是个线程安全的类。需要注意的是这里的Person类如果是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。

java监视器模式

遵循java监视器模式的对象会把所有可变状态都封装起来,并由对象的内置锁保护。

私有的锁对象:

//线程安全
public class PrivateLock{
    private Object myLock = new Object();
    Widget widget;
    void someMethod() {
        synchronized(myLock) {
            // 访问或修改Widget的状态
        }
    }
}

使用私有的锁对象而不是对象的内置锁,有以下优点:

  • 私有的锁对象可以将锁封装起来,使客户代码无法获得锁,但客户代码可以通过公有方法来访问锁,以便参与到它的同步策略中。

示例:车辆追踪

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 IllegalArgumentException("No such ID: "+ id);
        }
        loc.x = x;
        loc.y = y;
    }
    
    private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m) {
        Map<String,MutablePoint> res = new HashMap<String,MutablePoint>();
        for(String id : m.keySet()) {
            res.put(id,new MutablePoint(m.get(id)));
        }
        return Collections.unmodifiableMap(res);
    }
    
    // 这个类非线程安全
    public class MutablePoint {
        public int x,y;
        public MutablePoint() {
            x = 0;
            y = 0;
        }
        public MutablePoint(MutablePoint p) {
            this.x = p.x;
            this.y = p.y;
        }
    }
}

线程安全性的委托

public class CountingFactorizer{
    public AtomicLong value;
}

对于这样一个类,由于AtomicLong是线程安全的,而且CountingFactorizer只包含value一个状态,所以CountingFactorizer是线程安全的。我们将这个过程称为线程安全性的委托,CountingFactorizer将它的线程安全性委托给AtomicLong保证。

示例:基于委托的车辆追踪器

public class DelegatingVehicleTracker {
    private final ConcurrentMap<String,Point> locations;
    private final Map<String,Point> unmodifiableMap;
    
    public DelegatingVehicleTracker(Map<String,Point> points) {
        locations = new ConcurrentHashMap<String,Point>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    
    public Map<String,Point> 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);
        }
    }
}

class Point {
    public final int x,y;
    
    public Point(int x,int y) {
        this.x = x;
        this.y = y;
    }
}

如果使用最初的MutablePoint而不是Point类,就会破坏封装性,因为getLocations会发布一个指向可变状态的引用,而这个引用不是线程安全的。

委托给多个独立的状态变量

前面的委托都是针对单个状态变量,我们还可以将线程安全性委托给多个状态变量。

public class VisualComponent {
    private final List<KeyBoardListener> keyBoardListener = new CopyOnWriteArrayList<KeyBoardListener>();
    private final List<MouseListener> keyBoardListener = new CopyOnWriteArrayList<MouseListener>();
    
    public void addKeyBoardListener(KeyBoardListener listener) {
        keyBoardListener.add(listener);
    }
    public void addMouseListener(MouseListener listener) {
        MouseListener.add(listener);
    }
    public void removeKeyBoardListener(KeyBoardListener listener) {
        keyBoardListener.remove(listener);
    }
    public void removeMouseListener(MouseListener listener) {
        MouseListener.remove(listener);
    }
}

委托失效

当有约束条件时,委托容易失效

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("can't set low to "+ i + " > upper");
        }
        lower.set(i);
    }
    
    public void setUpper(int i) {
        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());
    }
}

NumberRange不是线程安全的,没有维持对下界和上界进行约束的不变性条件。

setLower 和 setUpper都尝试维持不变性,但是都没有做到,因为他们是采用“先检查后执行”的操作,但没有使用足够的加锁机制保证这些操作的原子性。如果一个线程调用setLower(5),另一个线程调用setUpper(4),如果执行时序错误,那么两个方法同时通过检查进行设置就会导致upper<lower。不符合约束。

如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。

发布底层的状态变量

根据类对底层状态变量施加的不变性条件,我们才可以发布这些变量从而使其他类能修改他们。

如果一个状态变量是线程安全的,并且没有任何不变性条件约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。

示例:发布状态的车辆追踪器

public class PublishingVehicleTracker {
    private final Map<String,SafePoint> locations;
    private final Map<String,SafePoint> unmodifiableMap;
    
    public PublishingVehicleTracker(Map<String,SafePoint> locations) {
        this.locations = new ConcurrentHashMap<String,SafePoint>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
    }
    
    public Map<String,SafePoint> getLocations() {
        return unmodifiableMap;
    }
    
    public SafePoint getLocation(String id) {
        return locations.get(id);
    }
    
    public void setLocation(String id,int x,int y) {
        if(!locations.containsKey(id)) {
            throw new IllegalArgumentException("invalid vehicle name:  " + id);
        }
        locations.get(id).set(x,y);
    }
}

class SafePoint {
    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;
    }
}

SafePoint(SafePoint p)获取p的拷贝,方法体内不直接调用SafePoint(int x,int y)的原因是因为避免产生竞态条件,私有构造函数可以避免这种竞态条件。(私有构造函数捕获模式)

PublishingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,不同的是Map中的元素是线程安全且可变的Safeoint,而并非不可变的。getLocation返回底层Map对象的一个不可变副本。调用者可以修改Map中的SafePoint值改变车辆的位置。

在现有的线程安全类中添加功能

假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加”操作。

因为需要的是一个线程安全的类,那么这种“先查询后执行”的操作就要是原子的.

最安全的方式是修改原始的类,但这通常无法做到。另一个方法就是拓展这个类,但是并非所有的类都像Vector这样将状态像子类公开。

public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if(absent) {
            add(x);
        } 
        return absent;
    }
}

客户端加锁机制

对于由Collections.synchronizedList封装的ArrayList,在原始类中添加一个方法或者对类进行扩展都不行,第三种策略是扩展类的功能,但不是扩展类本身,而是将扩展代码放入一个“辅助类”中。

一个错误的做法:

public class ListHelper<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;
    }
}

错误的原因是synchronized没有锁住list,在执行putIfAbsent时可能list会被另外一个线程修改。

如果想要正确执行该方法,必须使List在实现客户端加锁或外部加锁时使用同一个锁

客户端加锁是值,对使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。在这里X指的是list。

public class ListHelper<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;    
        }
        
    }
}

通过添加一个原子操作扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。客户端加锁却更加脆弱因为它将类X的加锁代码放到和X完全无关的其他类中。

组合

组合是一种更好的方法。

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();
    }
}

通过自身的内置锁增加了一层额外的加锁。我们使用了java监视器模式来封装现有List,并且只要在类中拥有指向底部List的唯一外部引用,就能确保线程安全性。

将同步策略文档化

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

在设计同步策略时需要考虑多个方面:例如,将哪些变量声明为volatile类型,哪些变量用锁来保护,哪些锁保护哪些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作必须是原子操作等。

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/83314281