7.2 线程安全

1 什么是线程安全
▪ 竞争条件:多个线程共享同一可变变量,而不协调它们正在做什么。 ▪ 这是不安全的,因为程序的正确性可能取决于其低级操作的时间安排。

▪ 如果从多个线程使用数据类型或静态方法时其行为正确(无论这些线程如何执行),并且不要求调用代码进行额外的协调,则该数据类型或静态方法是线程安全的。
▪ -“行为正确”意味着满足其规范并保持其rep不变量;
-“无论线程如何执行”意味着线程可能在多个处理器上,或者在同一个处理器上有时间间隔;
-“没有额外的协调”意味着数据类型不能在其调用方上设置与计时,比如“set()正在进行时不能调用get()。”
▪ 还记得迭代器吗?它不是线程安全的。
▪ 迭代器的规范指出,不能在迭代集合的同时对其进行修改。
▪ 这是给调用者设置的与时间相关的先决条件,迭代器不能保证在违反它的情况下行为正确。
remove()规范
▪ 考虑Java集合类,这些类通常在客户机和类的实现者上用非常清晰的契约进行记录。
–在迭代集合时不能修改集合。

在这里插入图片描述
线程安全的四种方法
▪ 限制数据共享.
▪ 共享不可变数据
▪ Threadsafe数据类型将共享数据封装在为您进行协调的现有Threadsafe data类型中。
▪ 同步
使用同步来防止线程同时访问变量。为实现自己定义的线 程安全的数据类型提供了支持

策略一:限制线程的数据共享
▪ 线程限制是一个简单的想法:
–通过将可变数据限制在单个线程中,可以避免对可变数据的争用。
–不要让任何其他线程直接读取或写入数据。
▪ 由于共享可变数据是竞争条件的根本原因,因此限制通过不共享可变数据来解决它。
–局部变量始终是线程限制的。本地变量存储在堆栈中,每个线程都有自己的堆栈。一次可能有多个方法调用在运行,但每个调用都有自己的变量私有副本,因此变量本身是受限的
。–如果局部变量是对象引用的,则需要检查它指向的对象。如果对象是可变的,那么我们要检查对象是否也被限制了
-不能有任何其他线程可以访问(不别名)的对它的引用。

内部Java内存模型
▪ JVM内部使用的Java内存模型在线程堆栈和堆之间划分内存。
在这里插入图片描述
▪ 在JVM中运行的每个线程都有自己的线程堆栈。
–线程堆栈包含有关线程已调用哪些方法以到达当前执行点的信息。
–它被称为“调用堆栈”。当线程执行其代码时,调用堆栈将更改。
▪ 线程堆栈还包含正在执行的每个方法(调用堆栈上的所有方法)的所有局部变量。
–线程只能访问自己的线程堆栈。
–由线程创建的局部变量对创建它的线程以外的所有其他线程都不可见。
–即使两个线程正在执行完全相同的代码,两个线程仍将在各自的线程堆栈中创建该代码的局部变量。
–因此,每个线程都有自己版本的每个局部变量。

内部Java内存模型:Heap
▪ 原始类型(boolean、byte、short、char、int、long、float、double)的所有局部变量都完全存储在线程堆栈中,因此对其他线程不可见。
▪ 一个线程可以将一个基元变量的副本传递给另一个线程,但它不能共享基元局部变量本身。
▪ 堆包含在Java应用程序中创建的所有对象,而不管是哪个线程创建的对象。
▪ 这包括原语类型的对象版本(例如Byte、Integer、Long等)。
▪ 不管对象是创建并分配给本地变量,还是创建为另一个对象的成员变量,对象仍然存储在堆中。
局部变量可以是基元类型,在这种情况下,它完全保留在线程堆栈中。
▪ 局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈上,而对象本身(如果存储在堆上)则存储在线程堆上。
▪ 对象可以包含方法,这些方法可以包含局部变量。这些局部变量也存储在线程堆栈中,即使方法所属的对象存储在堆中。
▪ 对象的成员变量与对象本身一起存储在堆中。如果成员变量是基元类型,并且是对对象的引用,则都是这样。
▪ 静态类变量也与类定义一起存储在堆中。

▪ 堆上的对象可以被所有引用该对象的线程访问。
▪ 当线程有权访问某个对象时,它也可以访问该对象的成员变量。
▪ 如果两个线程同时调用同一个对象上的方法,它们都可以访问该对象的成员变量,但每个线程都有自己的本地变量副本。
在这里插入图片描述
避免全局变量
▪ 这个类在getInstance()方法中有一个竞争——两个线程可以同时调用它,最后创建PinballSimulator对象的两个副本,这违反了rep不变量。 在这里插入图片描述
要使用线程限制方法修复此竞争,可以指定只允许特定线程调用getInstance()。
但Java并不能保证这一点。

▪ 假设有两个线程正在运行getInstance()。
▪ 对于两个线程对应执行的每一对可能行号,是否可能会违反不变量?在这里插入图片描述
▪ 全局静态变量不会自动限制线程。
–如果程序中有静态变量,则必须创建一个参数,使只有一个线程可以使用它们,并且必须清楚地记录这一事实。
▪ 更好的方法是,应该完全消除静态变量。

在这里插入图片描述
▪ isPrime()方法在多个线程中调用是不安全的,它的客户端甚至可能没有意识到这一点。
–原因是静态变量缓存引用的HashMap被所有对isPrime()的调用共享,而HashMap不是线程安全的
–如果多个线程同时改变映射,则通过调用缓存.put,则映射可能会以与银行帐户损坏相同的方式损坏。
–如果幸运的话,损坏可能会在哈希映射的深处导致异常,比如NullPointerException或IndexOutOfBoundsException。
–但它也可能只是悄悄地给出错误的答案。
如果一个ADT的rep中包含 mutable的属性且多线程之间 对其进行mutator操作,那么 就很难使用confinement策略 来确保该ADT是线程安全的

策略2:不变性
▪ 实现线程安全的第二种方法是使用不可变的引用和数据类型。
–不变性解决了由争用条件引起的共享可变数据问题,并简单地通过使共享数据不可变来解决它。
▪ 声明为final的变量是不可赋值且不可变的引用,因此声明为final的变量可以安全地从多个线程访问。
–只能读取变量,不能写入。
–因为这种安全性只适用于变量本身,而且我们仍然必须证明变量指向的对象是不可变的。

▪ 不可变对象通常也是线程安全的
▪ 我们之所以说“通常”,是因为当前的不变性定义对于并发编程来说过于宽松。
–如果类型的对象在其整个生命周期中始终表示相同的抽象值,则类型是不可变的。
–但这实际上允许类型自由变异其代表,只要客户看不到这些变异,例如有益的变异
–例如缓存、延迟计算和数据结构重新平衡
▪ 对于并发性,这种隐藏的变异是不安全的。
–使用有益变异的不可变数据类型必须使用锁使自身具有线程安全性。
更强的不变性定义
▪ 为了确信不可变数据类型在没有锁的情况下是线程安全的,我们需要对不可变性有一个更严格的定义:
–没有mutator方法
–所有字段都是私有的,并且是final的
–没有表示暴露–没有表示中可变对象的任何变化–甚至没有有利的变化
▪ 如果遵循这些规则,那么可以确信不可变类型也将是线程安全的。

不变性和线程安全性
▪ 假设正在检查指定为不可变的ADT,以确定其实现是否实际是不可变的和线程安全的。▪
需要查看以下哪些元素?
在这里插入图片描述

▪ 相比起策略1 Confinement,该策略2 Immutability允许有全局rep, 但是只能是immutable的。

策略3:使用线程安全数据类型
▪ 实现线程安全的第三个主要策略是在现有的线程安全数据类型中存储共享的可变数据。–当Java库中的数据类型是线程安全的时,它的文档将显式地声明这一事实。
▪ 在Java API中,发现两种执行相同操作的可变数据类型(一种是threadsafe,另一种不是)已变得很常见。原因是这句话指出:与不安全类型相比,threadsafe数据类型通常会导致性能损失。
在这里插入图片描述
▪ Java中的集合接口(List、Set、Map)有一些基本实现,这些实现不是线程安全的。ArrayList、HashMap和HashSet的实现不能从多个线程安全地使用。
▪ Collections API提供了一组包装器方法,使集合在线程安全的同时仍然是可变的。Java API装饰器——这些包装器有效地使每个方法相对于其他方法是原子的。
——一个原子动作有效地同时发生,它不会将其内部操作与其他动作的内部操作交叉,在整个动作完成之前,其他线程看不到任何动作的效果,因此看起来永远不会部分完成。

在这里插入图片描述
▪ 包装器实现将其所有实际工作委托给指定的集合,但在该集合提供的功能的基础上添加额外的功能。
▪ 这是decorator模式的一个示例(参见第5-3节)
▪ 这些实现是匿名的;库提供的不是公共类,而是staticfactorymethod。
▪ 所有这些实现都在Collections类中找到,该类仅由静态方法组成。 ▪ 同步包装器将自动同步(线程安全)添加到任意集合。
在这里插入图片描述
▪ 全局变量缓存导致线程之间的竞争条件。
private static Map<Integer,Boolean> cache =
Collections.synchronizedMap(new HashMap<>());

▪ 请确保丢弃对底层非线程安全集合的引用,并仅通过同步包装器访问它。
▪ 新的HashMap只传递给synchronizedMap(),从不存储在其他任何地方。
▪ 底层集合仍然是可变的,引用它的代码可以规避不变性。

▪ 尽管对集合本身的方法调用(get()、put()、add()等)现在是线程安全的,但从集合创建的迭代器仍然不是线程安全的。
▪ 因此不能使用iterator()或for循环语法:
for(字符串s:lst)
{。。}
//不是线程安全的,即使lst是同步列表包装器
▪ 这个迭代问题的解决方案是在需要迭代集合时获取集合的锁。
▪ 使用同步集合的方式仍可能有竞争条件。
▪ 考虑这段代码,它检查列表是否至少有一个元素,然后获取该元素:if (!lst.isEmpty()) { String s = lst.get(0); … }
▪ 即使将lst放入同步列表中,此代码仍然可能有竞争条件,因为另一个线在这里插入图片描述程可能会删除isEmpty()调用和get()调用之间的元素。

▪ 同步映射确保containsKey()、get()和put()现在是原子的,因此从多个线程使用它们不会损坏映射的rep不变量。
▪ 但是这三个操作现在可以以任意方式相互交错,这可能会打破is prime从缓存中需要的不变量:如果缓存将整数x映射到值f,那么x是素数,前提是f是true的。
▪ 如果缓存失败了这个不变量,那么我们可能返回错误的结果。
▪ 我们必须证明containsKey()、get()和put()之间的竞争不会威胁到这个不变量。
–containsKey()和get()之间的竞争并不有害,因为我们从不从缓存中删除项–一旦它包含x的结果,它将继续这样做。
–containsKey()和put()之间有一场比赛。结果,两个线程可能会同时测试同一个x的素数,并且两个线程都会争先恐后地用答案调用put()。但他们都应该用相同的答案调用put(),所以谁赢得比赛并不重要,结果将是相同的。
▪ 即使在使用threadsafedata类型时,也需要对安全性进行这些仔细的论证,这是并发性很难实现的主要原因。

▪ 在共享的可变数据上实现从比赛条件下的安全的三种主要方法:
–限制:不共享数据。
–不变性:共享,但保持数据不变。
–线程安全数据类型:将共享的可变数据存储在单个线程安全数据类型中。

5如何进行安全论证
开发ADT的步骤
▪ 指定:定义操作(方法签名和规范)。
▪ 测试:为操作开发测试用例。测试套件包括基于操作的参数空间划分的测试策略。
▪ Rep:选择一个Rep.
–首先实现一个简单的暴力Rep。
–写下rep不变量和抽象函数,并实现checkRep(),它在每个构造函数、生产者和mutator方法的末尾声明rep不变量。
▪ +++Synchronize–说明您的代表是线程安全的。
–在类中以注释的形式显式地写下来,直接写在rep不变量旁边,以便维护人员知道如何将线程安全性设计到类中。

–安全参数需要对模块或程序中存在的所有线程及其使用的数据进行编目,并说明使用的四种技术中的哪一种来防止每个数据对象或变量的竞争:限制、不变性、线程安全数据类型或同步。
–当使用后两种方法时,还需要说明对数据的所有访问都是适当的原子访问
–也就是说,依赖的不变量不会受到交错的威胁。

▪ 当我们就数据类型进行讨论时,限制通常不是一个选项,因为您必须知道系统中存在哪些线程,以及它们被授予了哪些对象的访问权限。
–如果数据类型创建自己的线程集,那么您可以讨论这些线程的限制。
–否则,线程从外部传入,承载客户端调用,并且数据类型可能无法保证哪些线程引用了哪些。
▪ 所以在那种情况下,禁闭不是一个有用的论据。
–通常我们使用更高级别的限制,将系统作为一个整体来讨论,并讨论为什么我们的某些模块或数据类型不需要线程安全性,因为它们在设计上不会跨线程共享。

▪ 这是一个很好的安全论证吗?

在这里插入图片描述
–Graph依赖于其他线程安全数据类型来帮助实现其rep
–这可以防止某些竞争条件,但不是全部,因为Graph的rep不变量包括节点集和边缘映射之间的关系。出现在边贴图中的所有节点也必须出现在节点集中

在这里插入图片描述
▪ 如果这段代码被执行了呢?
–此代码中有竞赛条件。当rep不变量被破坏时,有一个关键时刻,就在边映射发生变异之后,但就在节点集发生变异之前。
–图上的另一个操作可能在此时交错,发现rep不变量断开,并返回错误的结果。

在这里插入图片描述
▪ 尽管线程安全的set和map数据类型保证了它们自己的add()和put()方法是原子的、不干涉的,但它们不能将这种保证扩展到两个数据结构之间的交互。因此图的rep不变量在竞争条件下是不安全的。▪ 当rep不变量依赖于rep中对象之间的关系时,仅使用不可变和线程安全的可变数据类型是不够的。

在这里插入图片描述
▪ 如果这段代码被执行了呢?
–在if检查之后,但在Map中存储新的空集之前。另一个线程调用add edge(from,to2)从同一个from添加一条边,但将其添加到另一个to,因此它将自己的空集合添加到映射中并将to2放入其中。
–在将控件返回到原始线程时,原始线程现在用自己的新空集覆盖该集,丢弃从到2的边。

▪ 这篇文章讨论了在共享可变状态下从竞争条件中获得安全的三种主要方法:限制:不共享变量或数据。
–不变性:共享,但保持数据不变,变量不可赋值。
–线程安全数据类型:将共享的可变数据存储在单个线程安全数据类型中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值