2.对象的组合、基础构建模块

一、QA

1、如何设计一个线程安全的类?

  • 找出构成对象状态的所有变量
    • 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。例如,LinkedList的状态就包括该链表中所有节点对象的状态。
  • 找出约束状态的不变性条件、先验条件和后验条件,这些需要额外的同步与封装。
    • 不变性条件:约束了对象状态的取值范围,或者状态与状态之间存在不变性条件(例如obj.min必须要小于obj.max)。
    • 先验条件:一个对象状态必须处于某个值才能进行下一步操作。
    • 后验条件:一个对象状态的下一个值必须是某个值。
  • 建立对象状态的并发访问策略
    • 定义如何在不违背对象不变性条件或后验条件的情况下对状态的访问操作进行协同。同步策略规定了如何将不变性条件、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁保护。要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档。

2、什么是实例封闭?

  • 如果A类要使用一个线程不安全的B类作为A的状态变量,那么可以使B被封装在A中,不将B发布出去。也就是说将B的实例封装在了A实例中。这就是实例封闭。
  • 即使外部要访问B也是通过A提供出来的入口去访问,此时,结合合适的加锁策略,可以确保以线程安全的方式来使用非线程安全的对象。

3、写一个通过实例封闭的机制来确保线程安全的例子

    public class PersonSet {
        // HashSet 本身并不是线程安全的,但是此对象是封装在PersonSet对象中的,外部不能直接访问
        private final Set<Person> mySet = new HashSet<>();
    
        // 实例封闭结合加锁机制来访问非线程安全的对象   
        public synchronized void addPerson(Person person) {
            this.mySet.add(person);
        }
    
        public synchronized boolean containsPerson(Person person) {
            return mySet.contains(person);
        }
    
        // 这个示例并未对Person的线程安全性做任何假设,但如果Person类是可变的,
        // 那么再访问从PersonSet中获得的Person对象时,还需要额外的同步。
        static class Person {
            // TODO 。。。       
        }
    }

4、Java监视器是什么?Java监视器模式又是什么?

  • 在进入和退出同步代码块的字节指令也称为monitorenter和monitorexit,而java的内置锁也称为监视器锁或监视器。
  • java监视器模式是从线程封闭原则及其逻辑推论中得出的。遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

5、写一个Java监视器模式的例子

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

6、什么线程安全性的委托?如果一个类中的所有状态都是线程安全的,那么这个类是线程安全的吗?

  • 比如A类中的所有状态都是线程安全的,并且各个状态之间不存在耦合关系,那么这种可以说是将A类的线程安全性委托给了它的几个线程安全的状态变量。
  • 即使一个类的所有状态都是线程安全的,此类也不一定是线程安全的。比如这个类含有复合操作,那么仅靠委托线程安全的状态变量并不足以实现线程安全性。需要通过加锁机制来维护不变性条件以确保其线程安全性。

7、如何对现有的线程安全类添加新功能(不能修改源代码)?

  • 第一种方式:扩展这个类。在添加新方法时需要考虑到原来这个类的同步策略。
    • 这种方式缺点就是太依赖底层实现了:这个类是final的就无法扩展、持有的锁对象是私有的就无法与此类使用相同的同步策略、如果此类的同步策略变了也要跟着变
  • 第二种方式:客户端加锁机制。如果要使用这种方式,一定要注意与被保护对象使用同一把锁,否则你加的锁只是个假象。
  • 第三种方式:代理。与这个类实现相同的接口,将这个类组合到我的新类中,并对外提供一致的加锁机制来访问这个线程安全的类。此时可以对这个新类添加新的功能,只要加锁机制一致即可。

二、其他

  • 要保证一个类的线程安全性,必须要先了解该类的不变性条件与后验条件。

  • 良好的封装可以更好的保证线程安全,因为所有代码路径都是已知的。

  • 即便状态变量本身并不是线程安全的,也可以通过实例封闭保证安全性。

  • 私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便(正确的或不正确的)参与到它的同步策略中。如果客户代码错误的获得了另一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个类。

  • 通过代理模式去添加新功能不要依赖底层实现。

  • 同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。

  • 对同步容器类在进行迭代时,如果有线程并发的更改容器中的元素,会抛出ConcurrentModificationException。此时可以通过客户端加锁的方式,在迭代期间不允许其他线程修改容器中的元素。如果不希望在迭代期间对容器进行加锁,那么一种替代的方法就是“克隆”容器,并在副本上进行迭代。(在克隆过程中仍然需要对容器加锁)。在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。

  • 虽然加锁可以防止迭代器抛出ConcurrentModificationException,但是必须要记住在所有对共享容器进行迭代的地方都需要加锁。比如有些迭代操作比较隐蔽,比如对容器做toString操作,其内部也是迭代,如果此时有其他线程修改也会抛出异常。

三、示例代码

1、基于监视器的车辆跟踪器,性能较差

/**
 * 车辆跟踪器(线程安全性基于监视器)
 *
 * 虽然MutablePoint不是线程安全的,但是追踪器是线程安全的。它所包含的Map对象和MutablePoint都未曾发布。
 *
 * 由于deepCopy是从一个synchronized方法中调用的,因此在执行时间较长的复制操作中,
 * tracker的内置锁将一直被占有,当有大量车辆需要追踪时,会严重降低用户界面响应灵敏度。
 */
public class _4_4MonitorVehicleTracker {

    /**
     * 表示所有车辆的位置,key为车辆ID
     */
    private final Map<String, MutablePoint> locations;

    /**
     * 初始化车辆位置,对输入参数做深拷贝,防止Map逸出。
     */
    public _4_4MonitorVehicleTracker(Map<String, MutablePoint> locations) {
        this.locations = deepCopy(locations);
    }

    /**
     * 获取所有车辆的位置,返回深拷贝的locations,防止Map逸出。
     */
    public synchronized Map<String, MutablePoint> getLocations() {
        return deepCopy(locations);
    }

    /**
     * 获取某个车辆的目前的位置,返回的是拷贝的位置对象,防止MutablePoint逸出。
     */
    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;
    }

    /**
     * 深拷贝输入的Map
     */
    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        Map<String, MutablePoint> result = new HashMap<>();
        for (Map.Entry<String, MutablePoint> entry : m.entrySet()) {
            result.put(entry.getKey(), new MutablePoint(entry.getValue()));
        }
        return Collections.unmodifiableMap(result);
    }

    /**
     * 可变的Point类,表示车辆的坐标
     */
    static class MutablePoint {

        public int x, y;

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

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

}

2、基于委托线程安全类的车辆追踪器,性能较高

/**
 * 车辆跟踪器(线程安全性基于委托线程安全类)
 *
 * 由于Point类是不可变的,因而它是线程安全的,不可变的值可以被自由地共享与发布,因此在返回locations不需要复制。
 *
 * 在这个类中,没有使用任何显式的同步,所有对状态的访问都有ConcurrentHashMap来管理,而且Map的所有键和值都是不可变的。
 *
 * 在使用监视器模式的车辆追踪器中返回的是车辆的快照,
 * 而在使用委托的车辆跟踪器中返回的是一个不可修改但却实时的车辆位置视图。
 * 这可能是优点(更新的数据能够实时反映),也可能是缺点(可能导致不一致的车辆位置视图),这取决于具体需求。
 *
 * 如果需要一个不发生变化的车辆视图,那么getLocations可以返回对locations这个Map对象的一个浅拷贝,
 * 因为Map的内容是不可变的,因此只需要复制Map的结构,不需要复制它的内容。
 * public Map<String, Point> getLocations() {
 *     return Collections.unmodifiableMap(new HashMap<>(locations));
 * }
 *
 */
public class _4_6DelegatingVehicleTracker {

    /**
     * 保存车辆的位置信息,key为车辆ID,value为车辆坐标
     */
    private final ConcurrentMap<String, Point> locations;

    /**
     * 是locations的一个不可修改的视图
     */
    private final Map<String, Point> unmodifiableMap;

    public _4_6DelegatingVehicleTracker(Map<String, Point> points) {
        locations = new ConcurrentHashMap<>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    /**
     * 获取所有车辆的位置信息,返回的map是不可变的,所以也是防止locations直接对外暴露
     */
    public Map<String, Point> getLocations() {
        return unmodifiableMap;
    }

    /**
     * 返回某个车辆的位置信息,由于Point是一个不可变的类,所以不怕暴露出去,因为别人无法修改
     */
    public Point getLocation(String id) {
        return locations.get(id);
    }

    /**
     * 更新车辆的位置信息
     */
    public void setLocation(String id, int x, int y) {
        Point oldPoint = locations.replace(id, new Point(x, y));
        if(oldPoint == null) {
            throw new IllegalArgumentException("invalid vehicle name: " + id);
        }
    }

    /**
     * 表示车辆的坐标
     */
    static class Point {
        public final int x, y;

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

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容