网络表征_表征线程安全

在Joshua Bloch的出色著作《 有效的Java编程语言指南》 (请参阅参考资料 )中,第52项的标题为“文档线程安全”,其中,开发人员被要求以书面形式准确地编写该类所提供的线程安全保证。 就像布洛赫(Bloch)书中的大多数建议一样,这是极好的建议,通常会重复出现,但执行起来却很少。 (就像布洛赫(Bloch)在他的《 编程难题》演讲中所说,“别像我的兄弟一样编码。”)

您查看过Javadoc一类多少次,并且想知道“该类是线程安全的吗?” 在没有清晰文档的情况下,读者可能会对类的线程安全性做出错误的假设。 也许他们只是假设它在不是的时候是线程安全的(那真是太糟糕了!),或者他们会假设可以通过在调用其方法之一之前在对象上进行同步来使其成为线程安全的(这可能是正确,或者可能只是效率低下,或者在最坏的情况下,只能提供线程安全性的错觉。 无论如何,最好在文档中清楚说明在线程间共享实例时类的行为。

作为这种陷阱的一个示例,类java.text.SimpleDateFormat不是线程安全的,但是直到1.4 JDK才在Javadoc中进行了记录。 有多少开发人员错误地创建了SimpleDateFormat的静态实例,并从多个线程使用了它,却没有意识到他们的程序在重负载下无法正常运行? 不要对您的客户或同事这样做!

在忘记(或离开公司)之前写下来

记录线程安全性的时间肯定是在第一次编写该类时–与编写一个类相比,在几个月后重新评估该类时,评估一个类的线程安全性要求和行为要容易得多。 您将不会像编写时那样清楚地了解实现中发生的情况。 另外,在编写类时记录线程安全保证将增加将来的修改将保留您最初的线程安全意图的机会,因为维护人员希望将文档视为类规范的一部分。

如果线程安全性是类的二进制属性,那将很好,您可以仅记录该类是否是线程安全的。 但是不幸的是,这并不是那么简单。 如果一个类不是线程安全的,是否可以通过同步对该类对象的每次访问来使其成为线程安全的? 是否存在不能容忍来自其他线程的干扰的操作序列,因此不仅需要在基本操作上而且还需要在复合操作周围进行同步? 在方法之间是否存在状态依赖性,这些方法要求某些操作组以原子方式执行? 这是开发人员准备在并发应用程序中使用类时需要的信息。

定义线程安全

显然,很难定义线程安全性,而且大多数定义似乎都是彻头彻尾的循环。 Google的快速搜索显示了以下示例,这些示例是线程安全代码的典型但无用的定义(或描述):

  • 可以从多个编程线程中调用...,而无需在线程之间进行不必要的交互。
  • ...一次可以由多个线程调用,而无需调用方执行任何其他操作。

有了这样的定义,难怪我们对线程安全如此困惑。 这些定义并不比说“如果可以从多个线程安全地调用一个类,则该类是线程安全的”更好,这当然是什么意思,但这无助于我们从一个线程中分辨出一个线程安全的类。不安全的。 安全是什么意思?

实际上,任何关于线程安全的定义都必须具有一定程度的循环性,因为它必须符合类的规范-这是对类的用途,其副作用,状态有效或不正式的非正式散文描述。无效,不变式,前提条件,后置条件等。 (规范对对象状态施加的约束仅适用于外部可见状态-可以通过调用其公共方法并访问其公共字段来观察的状态-而不是其内部状态,实际上是其内部表示的状态私人领域。)

线程安全

要使一个类具有线程安全性,它首先必须在单线程环境中正确运行。 如果正确地实现了一个类,即表示它符合其规范的另一种说法,则对该类的对象进行的任何操作序列(对公共字段的读取或写入以及对公共方法的调用)都不能将其放入无效状态,观察对象处于无效状态,或违反类的任何不变式,前提条件或后置条件。

此外,要使一个类具有线程安全性,无论从运行时环境对这些线程的执行进行调度或交织,在从多个线程进行访问时,按照上述意义,它都必须继续正确地运行,而无需任何额外的操作调用代码方面的同步。 这样做的结果是,对线程安全对象的操作将以固定的全局一致顺序出现在所有线程上。

正确性和线程安全性之间的关系与描述ACID(原子性,一致性,隔离性和持久性)事务时使用的一致性和隔离性之间的关系非常相似:从给定线程的角度来看,似乎对对象执行的操作由不同的线程顺序执行(尽管顺序不确定),而不是并行执行。

方法之间的状态依赖性

考虑下面的代码片段,该代码片段迭代Vector的元素。 即使Vector所有方法都已同步,但在没有附加同步的情况下在多线程环境中使用此代码仍然不安全,因为如果另一个线程在恰好错误的时间删除元素,则get()可能会抛出ArrayIndexOutOfBoundsException

Vector v = new Vector();

    // contains race conditions -- may require external synchronization
    for (int i=0; i<v.size(); i++) {
      doSomething(v.get(i));
    }

这里发生的是, get(index)规范中有一个前提条件,该前提条件是index必须为非负且小于size() 。 但是,在多线程环境中,您无法知道上次观察到的size()值仍然有效,因此,除非您一直在对on i<size()持有排他锁,否则您就无法知道i<size() 。从上次调用size()之前的Vector

更具体地说,该问题源于以下事实: get()的前提条件是根据size()的结果定义的。 每当您看到这样的模式时,您必须使用一种方法的结果来调节另一种方法的输入,您就具有状态依赖性 ,并且必须确保至少在两个方法之间,状态的元素不会发生变化。 。 通常,唯一的方法是从调用第一个方法之前到调用最后一个方法之后,在对象上持有排他锁; 在上面的示例中,您在Vector各个元素之间进行迭代,您需要在迭代期间在Vector对象上进行同步。

螺纹安全度

如以上示例所示,线程安全并非全有或全无。 Vector的方法都是同步的,并且Vector被明确设计为可在多线程环境中运行。 但是其线程安全性受到限制,即某些方法对之间存在状态依赖性。 (类似地,如果Vector在迭代过程中被另一个线程修改,则Vector.iterator()返回的迭代器将引发ConcurrentModificationException 。)

对于Java类中通常发生的各种级别的线程安全,没有一组广泛接受的术语可用,但是重要的是您要尝试在编写类时记录它们的线程安全行为。

Bloch概述了一种分类法,该分类法描述了线程安全的五类:不可变的,线程安全的,有条件的线程安全的,线程兼容的和线程敌对的。 只要您清楚地记录线程安全特性,是否使用此系统都没有关系。 这个系统有局限性-类别之间的界限不是100%明确的,并且在某些情况下它没有解决-但是这个系统是一个很好的起点。 此分类系统的核心是,调用者是否可以或必须用外部同步来包围操作(或操作序列)。 以下各节介绍了这五类线程安全。

一成不变的

本专栏的普通读者听到我赞扬不变性的优点时,不会感到惊讶。 不变对象被保证是线程安全的,并且永远不需要额外的同步。 因为不可变对象的外部可见状态永远不会改变,所以只要正确构造它,就永远不会观察到它处于不一致状态。 Java类库中的大多数基本值类,例如IntegerStringBigInteger都是不可变的。

线程安全

线程安全对象具有上面“线程安全”部分中描述的属性-当多个线程访问该对象时,无论运行时环境如何调度线程,该类规范所施加的约束仍然成立任何其他同步。 这种线程安全性保证是一个很强的保证–许多类(例如HashtableVector )将无法满足这一严格的定义。

有条件的线程安全

我们在7月的文章“ 并发集合类 ”中讨论了条件线程安全性。 有条件的线程安全类是针对每个操作的线程安全类,但是某些操作序列可能需要外部同步。 条件线程安全性的最常见示例是遍历从HashtableVector返回的迭代器-这些类返回的故障快速迭代器假定在遍历迭代器时不会对基础集合进行突变。 为了确保其他线程在遍历期间不会使集合发生变化,迭代线程应确保在遍历整个过程中它具有对集合的独占访问权。 通常,通过同步锁来确保排他访问-并且类的文档应指定哪个锁(通常是对象的内部监视器)。

如果要记录条件线程安全的类,则不仅应记录它是条件线程安全的,而且还应保护哪些操作序列免受并发访问。 用户可以合理地假设其他操作序列不需要任何其他同步。

线程兼容

线程兼容类不是线程安全的,但是可以通过适当地使用同步在并发环境中安全地使用它们。 这可能意味着用synchronized块包围每个方法调用,或者在每个方法都被同步的情况下创建包装对象(例如Collections.synchronizedList() )。 或可能意味着用synchronized块围绕某些操作序列。 为了最大化线程兼容类的有用性,它们不应要求调用者在特定的锁上进行同步,而只是在所有调用中都使用相同的锁。 这样做将使在其他线程安全的对象中作为实例变量保存的线程兼容对象能够在拥有对象的同步时背负。

许多常见的类都是线程兼容的,例如集合类ArrayListHashMapjava.text.SimpleDateFormat或JDBC类ConnectionResultSet

线程敌对

线程敌对类是不能被安全地并发使用的类,无论调用了什么外部同步。 线程敌对的情况很少见,通常在类修改静态数据时发生,这种情况会影响可能在其他线程中执行的其他类的行为。 线程敌对类的一个示例是调用System.setOut()

其他线程安全文档注意事项

线程安全类(以及线程安全程度较低的类)可能会或可能不会允许调用者锁定对象以进行独占访问。 Hashtable类使用对象的内部监视器进行所有同步,但是ConcurrentHashMap类没有,实际上,没有办法锁定ConcurrentHashMap对象以进行独占访问。 除了记录线程安全程度之外,还应该记录任何特定的锁(例如对象的固有锁)在类的行为中是否具有特殊意义。

通过记录一个类是线程安全的(假设它实际上是线程安全的),您可以执行两项有价值的服务:您通知该类的维护者他们不应进行有损于其线程安全的修改或扩展,并通知用户无需外部同步即可使用的类。 通过记录类是线程兼容的或有条件的线程安全的,可以通知用户通过适当地使用同步,多个线程可以安全地使用该类。 通过记录一个类是线程有害的,您可以通知用户即使是外部同步,他们也不能从多个线程安全地使用该类。 在每种情况下,您都在防止潜在的严重错误,这些错误在发生之前很难找到并修复。

结论

类的线程安全行为是其规范的内在部分,应作为其文档的一部分。 因为尚没有描述类描述线程安全行为的声明方法,所以必须以文本形式进行描述。 虽然Bloch的用于描述类的线程安全程度的五层系统并未涵盖所有可能的情况,但这是一个很好的开始。 当然,如果每个类的Javadoc中都包含这种程度的线程行为,我们所有人都会过得更好。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp09263/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值