《Java Concurrency in Practice》中三个VehicleTracker例子的分析

每个例子后面有代码,大家可以先把代码粘出来或者开两个页面,先过一下例子的代码,然后一边看分析一遍看代码,上下拖动看的话效果不好。

欢迎拍砖和补充。

线程安全需求分析

三个例子都是关于车辆追踪的。他们使用了不同的方式来保证车辆追踪类的线程安全性。
我们知道,如果要写一个线程安全类,那么首先得明确这个类关于线程安全的需求。
那么这个类的线程安全需求就是:
访问线程要么能够看到写线程对location的x,y坐标完整的写入,要么看不到。不允许出现访问线程只看到写线程写了其中一个坐标。

比如线程A在访问location时,只看到了B线程对location的x或者y坐标的写入,那就破坏了这个类的线程安全性。

例子1(MonitorVehicleTracker )

对 线程不安全+可变 对象进行实例封闭和加锁

作者使用了实例封闭+加锁机制保证了MonitorVehicleTracker类的安全性。
实例封闭的意思就是将状态的访问路径限制在对象内部,
实例限制后,只要对这些状态的访问自始至终使用同一个锁,就能保证其线程安全性。

MonitorVehicleTracker的唯一状态:locations,是一个HashMap对象,大家都知道它是可变的,也是线程不安全的。

构造函数和getLocations都做了一次deepCopy。这两个deepCopy都是必须的。deepCopy保证了将locations对象封装在了MonitorVehicleTracker实例中,向外发布的只是一个拷贝的副本。想要访问locations这个状态,只能通过MonitorVehicleTracker对象,而所有的访问路径,都加上了锁。

deepCopy方法

返回结果使用了Collections.unmodifiableMap(map):这里不使用UnmodifiableMap,而是只是用deepCopy的HashMap也是可以的,但是文档一定写清楚,返回的是deepCopy的Map。不然站在调用者的角度,如果对其进行写操作,就不能获得期望的结果。

值得注意的值 文档也是维护线程安全的重要组成部分
就好比SynchronizedCollection的子类,这些同步容器在调用iterator方法时并没有加锁。
导致如果用户需要读写一致,那么在迭代的时候必须加锁,而且这个锁必须是创建的SynchronizedXXX对象本身。
如果对读写一致没那么敏感,那迭代的时候只需要处理一下ConcurrentModificationException即可。
这些都是在SynchronizedXXX文档上写的很清楚的。

由于MutablePoint是可变的,如果deepCopy在迭代时不对每一个location进行复制map.put(k, new MutablePoint(v.x,v.y));而是使用map.put(k, v);那getLocations发布出去的所有location就存在线程安全风险,因为在外部其他线程得到location之后有可能对其进行更新。getLocation是同样的道理,发布出去的MutablePoint一定是副本。

locations字段的final是不是必须的?

我认为不是,因为这里并不是希望把MonitorVehicleTracker变成不可变对象。
如果没有final,那就必须注意,要使用安全的方式来发布MonitorVehicleTracker对象。

代码

public class MonitorVehicleTracker {

    private final Map<String, MutablePoint> locations;

    public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
        this.locations = deepCopy(locations);
    }

    public synchronized Map<String, MutablePoint> getLocations() {
        return deepCopy(locations);
    }

    public synchronized MutablePoint getLocation(String id) {
        MutablePoint location = locations.get(id);
        return location == null ? null : new MutablePoint(location.x, location.y);
    }

    public synchronized void setLocation(String id, int x, int y) {
        MutablePoint point = locations.get(id);
        if (point != null) {
            point.x = x;
            point.y = y;
        } else {
            throw new IllegalArgumentException("No such Id:" + id);
        }
    }


    private Map deepCopy(Map<String, MutablePoint> locations) {
        Map<String, MutablePoint> map = new HashMap<>();
        locations.forEach((k, v) -> {
            map.put(k, new MutablePoint(v.x, v.y));
        });
        return Collections.unmodifiableMap(map);
    }
}

/**
 * 可变,线程不安全
 */
class MutablePoint {
    public int x, y;

    public MutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

例子2(DelegatingVehicleTracker)

此类对访问locations的所有方法都没有加锁,而是通过使用线程安全的ConcurrentHashMap来保证DelegatingVehicleTracker的线程安全性。相当于是把线程安全行委托给了ConcurrentHashMap。

getLocations使用了UnmodifiableMap作为视图返回。如果不使用UnmodifiableMap而是直接返回locations行不行?
我认为是可以的,毕竟locations是ConcurrentHashMap类型,它是线程安全的,并且作为DelegatingVehicleTracker的一个状态,并没有什么约束条件,或者不允许有的状态迁移操作。

这里使用UnmodifiableMap只是增强了封装性,意味着,你想修改车辆位置,那必须通过DelegatingVehicleTracker对象上的方法来操作。

作者还提到另一种方法:下边代码中的getCopyedLocations方法。
这个方法和getLocations方法的区别是前者在不能够实时地反应车辆位置的变化,而后者可以。

因为Collections.unmodifiableMap(new HashMap<>(locations));在new HashMap时做了putAll.它将locations的所有元素浅复制了一份。所以当locations有写入操作时,HashMap并不能得知。

Collections.unmodifiableMap(locations)是将locations的引用保存在了UnmodifiableMap中,
所以当locations有写入操作时,UnmodifiableMap可以立即看到。

代码

public class DelegatingVehicleTracker {

    private final ConcurrentHashMap<String, ImmutablePoint> locations;
    private final Map<String, ImmutablePoint> locationsView;

    public DelegatingVehicleTracker(Map<String, ImmutablePoint> points) {
        this.locations = new ConcurrentHashMap<>(points);
        this.locationsView = Collections.unmodifiableMap(this.locations);
    }

    public Map<String, ImmutablePoint> getLocations() {
        return locationsView;
    }

    public Map<String, ImmutablePoint> getCopyedLocations() {
        return Collections.unmodifiableMap(new HashMap<>(locations));
    }

    public ImmutablePoint getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new ImmutablePoint(x, y)) == null) {
            throw new IllegalArgumentException("No such id:" + id);
        }
    }
}

class ImmutablePoint {
    private final int x, y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

例子3(PublishVehicleTracker)

此类和DelegatingVehicleTracker的区别:

  1. 使用了线程安全的SafePoint。
  2. setLocation方法不再replace一个新构造的ImmutablePoint。
    因为SafePoint和ConcurrentHashMap都是线程安全的,

所以这几个方法都不需要额外的同步,或者复制,直接调用他们的修改状态的方法是没问题的。

代码

public class PublishVehicleTracker {

    private final ConcurrentHashMap<String, SafePoint> locations;
    private final Map<String, SafePoint> locationsView;

    public PublishVehicleTracker(Map<String, SafePoint> points) {
        this.locations = new ConcurrentHashMap<>(points);
        this.locationsView = Collections.unmodifiableMap(this.locations);
    }

    public Map<String, SafePoint> getLocations() {
        return locationsView;
    }

    public SafePoint getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (!locations.contains(id)) {
            throw new IllegalArgumentException("No such id:" + id);
        }
        locations.get(id).setXY(x, y);
    }
}

class SafePoint {
    private int x, y;

    public SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public synchronized int[] getXY() {
        return new int[]{x, y};
    }

    public synchronized void setXY(int x, int y) {
        this.x = x;
        this.y = y;
    }

}

相关推荐