4对象组合

对象组合的意义

  • 将一些现有的线程安全组件组合为更大规模的组件或程序,降低线程安全分析和维护的复杂度,提高线程安全的可靠性

设计线程安全类的三要素

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

对象状态

  • 对象所有域都是基本类型,那么这些域就构成对象的全部状态
  • 对象域中引用了其他对象,那么该对象的全部状态将包含被引用对象的域

同步策略

  • 定义:定义了如何在不违背对象的不变性条件和后验条件的情况下对其状态的访问操作进行协同
  • 要求:要确保开发人员可以对一个线程安全的类进行分析与维护,就必须将同步策略写成正式文档

不变性条件和后验条件约束的内容

  • 不变性条件:约束了有效的状态
    • 注意:如果一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁
  • 后验条件:约束有效的状态转换动作(例如自增操作i++,后一个值必须是前一个值加1)

依赖状态的操作

  • 例如:不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态
  • 如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作

实例封闭

  • 概念:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁
  • 应用:通过将封闭机制与合适的加锁锁机制结合起来,可以确保以线程安全的方式来使用非线程安全的对象
  • 作用域:
    1. 封闭在类的一个实例中(例如,作为类的一个私有成员)
    2. 封闭在某个作用于内(例如,作为一个局部变量)
    3. 封闭在线程内(例如,使用ThreadLocal把变量封闭在线程内)
  • 实例封闭是实现线程安全类的一个最简单方式
  • 实例封闭的优点:
    1. 在锁策略的选择上有了更多的灵活性,只要自始至终都是用同一个锁,就可以保护状态一致性
    2. 使得不同状态变量可以由不同的锁来保护
/**
 * 该类确保了mySet引用对象的线程安全性
 * 但是Person的线程安全性不能保证
 * 要保证Person类的线程安全性,必须使Person成为线程安全的类
 */
public class PersonSet {
    private final Set<Person> mySet = new HashSet<Person>();
    
    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }
    
    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }
}

构造线程安全类的两种方式

  1. Java监视器模式
  2. 线程安全性委托

Java监视器模式

  • 概念:Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁来控制对对象状态的访问,都可以用来保护对象的状态
  • 主要优势:简单性

私有锁优点

  1. 私有锁可以将锁封装起来,客户端代码无法得到锁,但是客户端可以通过公有方法来访问锁,以便参与到它的同步策略中
  2. 判断锁在程序中是否被正确的使用,私有锁只需要检查单个类,而公有锁需要检查整个应用程序
/**
 * 私有锁示例
 */
public class PrivateLock {
    private final Object myLock = new Object();
    Widget widget;
    
    void someMethod() {
        synchronized(myLock) {
            //访问或修改Widget的状态
        }
    }
}

Java线程监视器模式示例

/**
 * Created by weicm on 2017/8/9.
 * 可变位置,线程不安全
 */
public class MutablePoint {
    public int x, y;
    public MutablePoint() {
        x = 0;
        y = 0;
    }

    /**
     * 位置对象拷贝
     * @param p
     */
    public MutablePoint(MutablePoint p) {
        this.x = p.x;
        this.y = p.y;
    }
}

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
 * Created by weicm on 2017/8/9.
 * 基于Java监视器的线程安全的车辆追踪器
 *
 * 介绍:
 * 线程安全实现方式:这种实现方式是通过在返回给客户端代码之前复制可变数据来维持线程安全性
 * 缺点:如果车辆容器太大,getLocations会由与deepCopy会导致低性能
 * 实现方式的好坏:如果结果需要保持一致性,那么这种方式很适合,如果结果要保持实时性,那么这种方式不符合要求
 */
public class MonitorVehicleTracker {
    //追踪器状态
    private final Map<String, MutablePoint> locations;

    /**
     * 通过深度拷贝传入的追踪器信息,初始化追踪器状态,避免追踪器状态逸出
     * @param locations
     */
    public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
        this.locations = deepCopy(locations);
    }

    /**
     * 返回只读追踪器镜像,避免追踪器状态逸出
     * @return
     */
    public synchronized Map<String,  MutablePoint> getLocations() {
        return deepCopy(locations);
    }

    /**
     * 获取位置镜像,避免位置状态对象逸出
     * @param id
     * @return
     */
    public synchronized MutablePoint getLocation(String id) {
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }
    /**
     * 通过深度拷贝追踪器创建器镜像,返回只读镜像
     * @param m
     * @return
     */
    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        HashMap<String, MutablePoint> result = new HashMap<>();
        for (String id: m.keySet())
            result.put(id, new MutablePoint(m.get(id)));
        return Collections.unmodifiableMap(result);
    }
}

线程安全性委托示例

/**
 * Created by weicm on 2017/8/10.
 * 不可变位置,线程安全
 */
public class Point {
    public final int x, y;

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


import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by weicm on 2017/8/10.
 * 基于安全委托的线程安全的车辆追踪器
 */
public class DelegatingVehicleTracker {
    //追踪器状态
    private final ConcurrentHashMap<String, Point> locations;
    //追踪器 实时 只读 对象(并不是状态对象,只做为应答结果,由与是只读的,所以安全)
    private final Map<String, Point> unmodifiableMap;

    /**
     * 初始化,将状态对象委托给线程安全的类
     * 同时,构造实时只读状态对象
     * @param locations
     */
    public DelegatingVehicleTracker(Map<String, Point> locations) {
        this.locations = new ConcurrentHashMap<>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    /**
     * 返回实时只读状态对象,安全
     * @return
     */
    public Map<String, Point> getLocations() {
        return unmodifiableMap;
    }

    /**
     * 通过委托的线程安全对象,获取不可变位置状态,安全
     * @param id
     * @return
     */
    public Point getLocation(String id) {
        return locations.get(id);
    }

    /**
     * 通过委托的线程安全对象,设置新的不可变位置状态,安全
     * @param id
     * @param x
     * @param y
     */
    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);
        }
    }
}

委托多个线程安全的状态变量

  • 如果一个类是有多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给这些底层的状态变量
  • 反面示例
    import java.util.concurrent.atomic.AtomicInteger;
    /**
     * 尽管NumberRange类状态变量委托给了线程安全的两个AtomicInteger类
     * 但是由于存在复合操作,并不足以保护它的不变性条件
     */
    public static 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 lower 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 isRange(int i) {
            //注意:不安全的“复合操作”
            return (i >= lower.get() && i <= upper.get());
        }
    }
    

发布底层状态变量

  • 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束他的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量
    /**
     * Created by weicm on 2017/8/10.
     * 线程安全且可变的SafePoint类
     */
    public 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;
        }
    }
    
    import java.util.Collections;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * Created by weicm on 2017/8/10.
     * 安全发布底层状态的车辆追踪器
     */
    public class PublishingVehicleTracker {
        //追踪器状态
        private final Map<String, SafePoint> locations;
        //追踪器 实时 只读 对象(并不是状态对象,只做为应答结果,由与是只读的,所以安全)
        private final Map<String, SafePoint> unmodifiableMap;
    
        /**
         * 初始化,将状态对象委托给线程安全的类
         * 同时,构造实时只读状态对象
         * @param locations
         */
        public PublishingVehicleTracker(Map<String, SafePoint> locations) {
            this.locations = new ConcurrentHashMap<>(locations);
            this.unmodifiableMap = Collections.unmodifiableMap(locations);
        }
    
        /**
         * 返回实时只读状态对象,安全
           同时也发布了unmodifiableMap中的SafePoint对象,由与SafePoint对象是线程安全的,所以可以安全的发布,原因同getLocation方法
         * @return
         */
        public Map<String, SafePoint> getLocations() {
            return unmodifiableMap;
        }
    
        /**
         * 通过委托的线程安全对象,获取不可变位置状态,安全
         * 发布了底层状态对象SafePoint,由于以下原因,所以PublishingVehicleTracker仍然是线程安全的
         *      1. 由于SafePoint是线程安全的
         *      2. 没有不变性条件约束SafePoint的有效值
         *      3. SafePoint只有set一个状态转换,不存在不允许的状态转换
         * @param id
         * @return
         */
        public SafePoint getLocation(String id) {
            return locations.get(id);
        }
    
        /**
         * 通过委托的线程安全对象,设置新的不可变位置状态,安全
         * @param id
         * @param x
         * @param y
         */
        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);
        }
    }
    

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

  1. 修改原始类(最安全,需要了解类的同步策略)
  2. 扩展类(较修改原始类更脆弱,因为同步策略分布在多个相关的单独维护的源代码中)
  3. 客户端加锁(较扩展类更脆弱,因为同步策略分布在多个不相关的单独维护的源代码中)
  4. 组合,将类的操作封装起来,用组合类的内置锁来实现同步策略,而不关心被封装的类是否是线程安全的(安全,由与封装对象可能是线程安全的,所以可能有轻微性能损失)

良好的习惯

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

推荐阅读更多精彩内容

  • 4.1 设计线程安全的类 在设计线程安全的类的过程中,需要包含以下的三个基本的要素: 找出构成对象状态的所有变量 ...
    大海孤了岛阅读 303评论 1 0
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,223评论 4 56
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 当我写出这个标题,我想说明什么,当温饱满足不了,哪里有尊重可谈,这一切的因果决定我想要的最后只有自己辛勤的劳动,尊...
    Serene汤先允阅读 226评论 0 0
  • See how much I love you.you are the apple in my eyes. Wha...
    紫罗兰原创馆阅读 539评论 0 1