[Java多线程编程之十二] 线程安全类的设计与使用

  在并发编程中,经常需要编写线程安全的类,设计线程安全类,优先考虑使用JUC包中封装的各种线程安全类。在不能满足需求自行封装的情况下,按三步走:分析共享变量、分析不变性条件、设计并发管理策略。需要对线程安全类进行扩展时,优先级为:修改源码 > 组合 > 继承。

一、设计线程安全类

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

  • 找出构成对象状态的所有变量。
  • 找出约束状态变量的不变性条件。
  • 建立对象状态的并发管理策略。

1、收集同步需求

准则:不会改变的域应该声明为final,有利于简化对象状态的分析。

不可变条件:判断状态是有效还是无效。

  比如商品的价格必须正的,计数器的次数必须是正的,但是保存商品价格、计数器次数的变量类型如double、int的取值范围却可以是负数的,这就要求在封装了这些状态的线程安全类里去控制保持不可变条件。

后验条件:判断状态迁移是否有效。

  比如账户余额是100,转入了50块钱,同时有因为购买交易转出了30块钱,最终账户余额肯定是120,如果多笔交易并发,没有控制好后验条件,最终账户余额就是错误的。

2、依赖状态的操作

先验条件:执行操作时判断状态是否满足条件。

  有些操作需要先判断当前状态是否满足,否则必须阻塞或无法执行,比如账户余额100,要购买一个200块钱的商品,肯定要先检查账户余额是否足够,否则应该报错,如果没控制好先验条件,用少于商品价格的金额买到了商品,就会引发账务问题。

3、状态的所有权

  所有权和封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。所有权意味着控制权,如果程序发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是“共享所有权”。

// 独占所有权
public class ExclusiveOwnership {
    public String[] status = {"start", "running", "stop", "terminal"};

    public ExclusiveOwnership() {}

    public String[] getStatus() {
        String[] ret = new String[status.length];
        for (int i = 0; i < status.length; i++)
            ret[i] = status[i];
        return ret;
    }
}

// 共享所有权
public class SharedOwnership {
    public String[] status = {"start", "running", "stop", "terminal"};

    public SharedOwnership() {}

    public String[] getStatus() {
        return status;
    }
}

  为了协调封装的多个状态的一致性,状态变量的所有者通常需要采用加锁协议来维持变量状态的完整性。

public class SafePoint {
    private int x, y;

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }

    // 拷贝构造函数
    // 如果实现为this(p.x, p.y)会产生竞态条件
    // 因为该实现中会前后分别两次去读取x、y,有可能读取到x之后坐标被改变了
    // 第二次读取到的y跟第一次不匹配
    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;
    }
}

  对于构造函数或者方法通过参数传递进来的对象,除非这些方法是被专门设计为转移传递进来的对象的所有权的(例如同步容器封装器的工厂方法),否则类通常并不拥有这些对象。

public class SharedOwnership {
    public String[] status;
    public SharedOwnership(String[] status) {
        this.status = status;
    }
}



二、实现线程安全类的方式

1、实例封闭

  实例封闭就是将数据(通常是非线程安全类的对象)封装在对象内部,这样可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁

  这是最常见的、最快速的线程安全类的实现方式,比如Java集合中的工具类CollectionsSychronized开头的方法,都是使用了实例封闭的方式来快速生成一个线程安全的类,比如SychronizedMap(),使用了内部静态类SynchronizedMap,该类对传入的Map对象进行封装,并使用类的mutex成员作为锁,对每个方法都进行了加锁同步,代码如下所示:


  封闭机制更易于构造线程安全的类,因此当封闭类的状态时,在分析类的线程安全性时就无需检查整个程序,上面的代码的这种设计类似典型的Java监视器模式,遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护,当然上面的代码用的不是SynchronizedMap类的内置锁,而是定义了另一个成员mutex来当锁,但是策略是类似的。

2、线程安全委托

  线程安全委托,就是将线程安全性保障委托给线程安全类实现。比如现在要实现一个线程安全的缓存类Cache,根据key来获取value,这种一对一映射关系适合用Map来实现,可以用HashMap + sychronized简单实现数据缓存和线程安全,即上面用的封闭实例的监视器模式,但是性能上存在瓶颈,如果将其委托给ConcurrentHashMap实现,就能在实现数据缓存和线程安全的同时,还能有性能上的提升,因为ConcurrentHashMap使用细粒度的分段锁来支持多线程并发操作。

  代码如下所示:

public class SafeCache {
    private final ConcurrentHashMap<String, Object> cacheMap = new ConcurrentHashMap<String, Object>();

    public SafeCache() {}

    public Object get(String key) {
        return cacheMap.get(key);
    }

    public void set(String key, Object value) {
        cacheMap.put(key, value);
    }
}

  当然,有的时候线程安全类未必能满足全部需求,比如要保护同步的状态有多个,且多个状态间存在关联关系,那么仅依靠委托并不足实现线程安全性,这时设计类的时候就要提供必须的加锁机制以保证这些复合关联操作都是原子操作。

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


3、扩展现有线程安全类功能

  设计线程安全类,优先选择重用现有的线程安全类而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及维护成本。但是很多时候,线程安全类不能完全满足我们的要求,比如要做一些功能的扩展,这时就有几种策略:

  • (1)能修改源码的情况下,直接修改源码。
  • (2)不能修改源码的情况下,比如Java类库中的类,优先级为:组合 > 客户端加锁 > 继承。
3.1 继承

  为Vector扩展一个若没有则添加的功能,代码如下:

/**
 * 同步策略被分布到多个独立维护的源码中
 * 底层类的同步策略会影响封装类同步策略的正确性
 *
 */
public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putifAbsent(E x) {
        boolean absent = !contains(x);
        if (absent)
            add(x);
        return absent;
    }

    // other code
}

  在很多类库中,都明确声明了类不可继承,这是一种防止类库使用人员误用写出脆弱代码的安全策略。

3.2 客户端加锁
public class BetterVector2<E> {
    private Vector<E> vector;

    public BetterVector2(Vector<E> vector) {
        this.vector = vector;
    }

    public synchronized boolean putifAbsent(E x) {
        boolean absent = !vector.contains(x);
        if (absent)
            add(x);
        return absent;
    }
    
    // other code
}

  客户端加锁存在的问题和继承类似,而且有些线程安全类是无法通过监视器锁去扩展功能的,比如ConcurrentHashMap并没有使用Java监视器锁,而是内置了分段锁,这种情况下,使用sychronized(mapObj)根本无法保证线程安全。

3.3 组合
/**
 * 通过组合的方式,统一同步策略的使用
 */
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 contain = list.contains(x);
        if (!contain)
            list.add(x);
        return !contain;
    }

    // other code
}


使用客户端加锁和继承的缺陷:

  • 加锁机制分散到多个类文件中
  • 破坏同步策略的封装性
  • 依赖于基类的同步策略,一旦策略有变,比如从用监视器锁改成内置锁,扩展类的同步策略会失效


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