资讯专栏INFORMATION COLUMN

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

binaryTree / 713人阅读

摘要:线程安全需求分析三个例子都是关于车辆追踪的。他们使用了不同的方式来保证车辆追踪类的线程安全性。值得注意的值文档也是维护线程安全的重要组成部分。

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

欢迎拍砖和补充。

线程安全需求分析

三个例子都是关于车辆追踪的。他们使用了不同的方式来保证车辆追踪类的线程安全性。
我们知道,如果要写一个线程安全类,那么首先得明确这个类关于线程安全的需求。
那么这个类的线程安全需求就是:
访问线程要么能够看到写线程对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对象。

安全发布参见原书3.5节或者这个例子https://segmentfault.com/q/10...)

代码
public class MonitorVehicleTracker {

    private final Map locations;

    public MonitorVehicleTracker(Map locations) {
        this.locations = deepCopy(locations);
    }

    public synchronized Map 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 locations) {
        Map 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 locations;
    private final Map locationsView;

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

    public Map getLocations() {
        return locationsView;
    }

    public Map 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的区别:

使用了线程安全的SafePoint。

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

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

代码
public class PublishVehicleTracker {

    private final ConcurrentHashMap locations;
    private final Map locationsView;

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

    public Map 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;
    }

}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67854.html

相关文章

  • 学习Java必读10本书籍

    摘要:学习编程的本最佳书籍这些书涵盖了各个领域,包括核心基础知识,集合框架,多线程和并发,内部和性能调优,设计模式等。擅长解释错误及错误的原因以及如何解决简而言之,这是学习中并发和多线程的最佳书籍之一。 showImg(https://segmentfault.com/img/remote/1460000018913016); 来源 | 愿码(ChainDesk.CN)内容编辑 愿码Slo...

    masturbator 评论0 收藏0
  • Treiber Stack简单分析

    摘要:在添加新项目时使用堆栈,将堆栈的顶部放在新项目之后。当从堆栈中弹出一个项目时,在返回项目之前,您必须检查另一个线程自操作开始以来没有添加其他项目。比较和交换将堆栈的头部设置为堆栈中旧的第二个元素,混合完整的数据结构。 Abstract Treiber Stack Algorithm是一个可扩展的无锁栈,利用细粒度的并发原语CAS来实现的,Treiber Stack在 R. Kent T...

    junfeng777 评论0 收藏0
  • 深入理解Java内存模型(六)——final

    摘要:对于域,编译器和处理器要遵守两个重排序规则在构造函数内对一个域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。这个屏障禁止处理器把域的写重排序到构造函数之外。下一篇深入理解内存模型七总结 与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个fi...

    lixiang 评论0 收藏0
  • Java 8 并发: Threads 和 Executors

    摘要:能够异步的执行任务,并且通常管理一个线程池。这样我们就不用手动的去创建线程了,线程池中的所有线程都将被重用。在之后不能再提交任务到线程池。它不使用固定大小的线程池,默认情况下是主机的可用内核数。 原文地址: Java 8 Concurrency Tutorial: Threads and Executors Java 5 初次引入了Concurrency API,并在随后的发布版本中...

    J4ck_Chan 评论0 收藏0

发表评论

0条评论

binaryTree

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<