导图社区 并发线程安全性知识点学习笔记
并发线程安全性知识点学习笔记:无状态对象,线程安全的原因:为什么说,多个线程访问同一个无状态对象,就好像访问不同的实例一样,也即好像没有访问共享的对象一样。
编辑于2022-11-22 11:58:35 广东并发线程安全性知识点学习笔记
第零节
一、理解
1)理解前提术语
1、共享
①什么是状态共享
某个变量可以被多个线程访问
2、可变
①什么是状态可变
变量的值在其生命周期内可以发生变化
3、状态
①什么是状态变量
②有哪四种状态变量
①既共享又可变(需要管理) ②共享但不可变(不需要管理) ③不共享但可变(不需要管理) ④既不共享又不可变(不需要管理) 要编写线程安全的代码, 其核心在于要对状态访问的操作进行管理,特别是对共享的(Shared) 和可变的(Mutable) 状态的访问
2)理解线程安全性
1、出现线程安全问题
①本质原因
①情况:多线程对某个对象『可变状态』的访问,在这种情况下,这个数据既是共享的,又是可变的 ②存在的问题? 对于共享且可变的数据进行访问的时候,如果无法对多个线程实现协同,也就是同步,那么可能会导致数据破坏以及其他不该出现的结果,也就是使得程序失去正确性,也就是带来线程安全性的问题
②如何判断线程是否安全
①和对象要实现的『功能无关』 ②和程序中对象的『访问方式有关』 一个对象是否需要是线程安全的,取决于它是否被多个线程访问
2、三种解决线程安全问题的思路
如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步那么程序就会出现错误。有三种方式可以修复这个问题 ①不共享:不在线程之间共享该状态变量 ②不可变:将状态变量修改为不可变的变量 ③既共享又可变,但是需要同步 在访问状态变量时使用同步
3、java中的几种同步机制
①synchronized:Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式 ②volatile类型的变量 ③显式锁(Explicit Lock) ④原子变量 ⑤注意:同步这个术语不仅仅包括synchronized
第一节 线程安全性
一、定义
1)复杂的定义
2)困惑的定义
①可以在多个线程中调用,并且在线程之间不会出现错误的交互 ②可以同时被多个线程调用,而调用者无须执行额外的动作
1、这些定义让人困惑的两个原因
因为它们没有告诉我们—— ①如何区分『线程安全的类』以及『非线程安全的类』 ②定义中“安全”的含义是什么
2、如何定义线程安全,才不会困惑
在线程安全性的定义中,最核心的概念就是『正确性』。如果对线程安全性的定义是模糊的,那么就是因为缺乏对『正确性』的清晰定义
3)理解前提
1、理解『正确性』
①什么是『正确性』
正确性的含义是,某个『类的行为』与『类的规范』完全一致。在良好的规范中通常会定义(但是通常我们自己不会定义) ①各种『不变性条件』(Invariant)来约束对象的状态 ②定义各种『后验条件』(Postcondition) 来描述对象操作的结果
3)正确的定义
1、简易版
当多个线程访问某个类时,这个类始终都能表现出正确的行为(也即始终满足,类的不变性条件和后验性条件),那么就称这个类是线程安全的
2、完全版
二、举例
1)因数分解 servlet
1、功能说明
这个 Servlet 从请求中提取出数值,执行因数分解,然后将结果封装到该 Servlet 的响应中
2、代码举例
@ThreadSafe public class StatelessFactorizer implements Servlet { public void service(ServletRequest req, ServletResponse resp) { Biglnteger i = extractFromRequest(req); Biglnteger[] factors = factor(i); encodelntoResponse(resp, factors); } }
3、此 servlet 线程安全的原因
StatelessFactorizer 是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。(你可以点开 servlet 看一看,servlet 是一个接口,它没有任何属性)
4、无状态对象,线程安全的原因
①只有局部变量
A、如何理解局部变量,不会产生线程安全性问题
局部变量因为其存储位置决定了,它永不共享:计算过程中的临时状态仅存在于线程栈上的局部变量中(我们知道静态变量、局部变量、属性都是存放在计算机中的不同的位置),并且只能由正在执行的线程访问
②没有共享变量
A、如何理解无状态对象,没有共享状态?
多个线程访问同一个对象时,成员变量会被共享,但是无状态对象没有成员。别说共享状态了,无状态对象连成员变量都没有,连状态都没有,怎么可能有共享状态
③为什么说,多个线程访问同一个无状态对象,就好像访问不同的实例一样,也即好像没有访问共享的对象一样
访问 StatelessFactorizer 对象的线程不会影响,另一个访问同一个 StatelessFactorizer 对象的线程,的计算结果,因为这两个线程并没有共享状态( servlet 根本没有任何状态,更别说共享状态了),就好像它们都在访问不同的实例(虽然都是访问的同一个 servlet『 StatelessFactorizer 』实例)
第二节 原子性
一、新的需求
假设我们希望为 servlet,增加一个『计数器』来统计所处理的请求数量
二、解决方案
1)叙述
一种直观的方法是在 Servlet 中增加一个 long 类型的成员属性,并且每处理一个请求就将这个值加1
2)代码
@NotThreadSafe public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount(){ return count; } public void service(ServletRequest req, ServletResponse resp) { Biglnteger i =extractFromRequest(req); Biglnteger[] factors = factor(i); ++count; encodelntoResponse(resp, factors) ; } }
3)可能的错误
1、起初
如果计数器的初始值为9
2、过程
那么在某些情况下,多个线程读到的值都为9(假如100核处器),同时读取了 count 属性,接着执行递增操作,并且都将计数器的值设为10
3、最后
结果肯定是错误的,处理了多个请求,为什么 servlet 对象的 count 属性的值,都是(++操作中的写入操作)写入了10,也即最终结果只增加了1,显然,这并不是我们希望看到的情况
4、错误原因
①原因:没有满足类的后验证性条件,也即破坏了类的正确性:存在多个线程访问后,访问次数只是加了1的情况 ②结论:因而这个 servlet 就不是线程安全的
三、竞态条件
1)概念
1、什么是『竞态条件』
①概念
在并发编程中,可能会因为出现『不恰当的执行时序』而出现『不正确的结果』,『这样的情况』就被称作『竞态条件』。
②特点
对于『竞态条件』来说,是否执行正确全凭运气。如果执行时序正确,那么计算就正确,如果执行时序不正确,计算就错误。
③解决思路
为了防止『竞态条件』出现不正确的结果,就需要规避『不恰当的执行时序』,那么如何规避『不恰当的执行时序』就是我们需要研究的课题
2、什么时候会出现『竞态条件』
当某个计算的正确性,取决于多个线程的『交替』『执行时序』时,那么就可能会发生竞态条件,因为存在线程的交替执行,因而可能会因为运气,产生不同的执行时序(根据线程的调度情况)。除非这多个线程的『交替』『执行时序』是正确的。
3、为什么竞态条件,会带来线程安全性问题
在多线程程序中,对于共享可变的状态进行访问时,由于执行时序的不同,可能会带来不同的结果——也就是说竞态条件,有一定的概率造成程序错误,影响程序的正确性(先验性条件、后验性条件),因而竞态条件会带来线程安全性的问题
2)分类
1、『先检查后执行』类型竞态条件
①为什么『先检查后执行』类型是竞态条件中最常见的
①说法一:多个线程的执行时序,会因为线程调度的不确定性,得到不同的检测结果,而不同的检测结果又带来了不同的执行结果 ②说法二: A、执行结果——依赖于——》检测的结果,而检测结果——依赖于——》多个线程的执行时序 B、多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种情况(当然也就包括,错误的情况)
②本质
多线程由于执行时序的不确定性,导致多线程所访问的类,会基于一种『可能失效』的观察结果,进而做出判断或者执行某个计算(如果观察结果合理,就正确,如果观察结果失效,就错误)
③举例(没有做思维导图,今后做)
现实生活中『先检查后执行』类型的竞态条件
延迟初始化中的『先检查后执行』类型的竞态条件
2、『读取-修改-写入』类型竞态条件(没有做思维导图,今后做)
3)存在的问题
执行时序、竞态条件与线程安全的关系
①针对某一对象共享且可变的状态进行访问——》执行时序的不确定性——》存在竞态条件(有因为时序的不确定带来的错误的可能性)——》线程不安全 ②如何保证线程安全? 要保证线程安全,就需要规避竞态条件,要规避竞态条件,就需要防止出现『执行时序的不确定性』。为了防止出现『执行时序的不确定性』,就需要确保『针对同一状态,进行改变』这一复合操作的原子性,要实现复合操作的原子性,就需要对多个线程『访问同一状态变量』实现协同
4)解决方案
1、解决『竞态条件』引起的线程安全性问题的『思路』
①思路叙述
要解决线程安全性问题——》就要解决『竞态条件』问题——》要解决『竞态条件』问题——》就要解决执行时序不确定的问题——》要解决执行时序不确定的问题——》就要知道 A、什么是『正确的执行时序』 B、使用什么方法,保证『正确的执行时序』 ——》使用复合操作,确保操作的原子性,可以保证正确的执行时序
②思路解释
A、为什么,要解决『竞态条件』问题,就要解决执行时序不确定的问题?而不是把多线程变为单线程?
①首先:你不能把多线程变成单线程 抓住问题的源头,我们知道多线程开发方式,是产生一切问题的根源。但是,多线程开发是必须要进行的。因为多线程开发具有很多优点。你不能因为多线程中程序会因为不当的编码带来『竞态条件』,就把多线程改为单线程。 ②然后:你不能修改对象的『共享』『可变』的性质 多线程开发方式,又经常伴随着对『共享』『可变』状态的访问,你总不能让所有的多线程开发,都是访问不共享或者不可变的的对象状态吧 ③最后:因此前面两个步骤,你都无法做文章。 所以,你必须在这样的情况下,去思考如何解决『竞态条件』问题——在多线程的情况下,多个线程会访问共享可变的状态变量。这两个因素是永远存在的,你只能着眼于其它地方,考虑如何解决『竞态条件』问题。于是,很自然地你就会考虑,通过解决执行时序不确定的问题,来规避『竞态条件』,从而避免出现线程安全性问题
B、使用什么方法,解决执行时序不正确的问题?
在访问『共享』『可变』的状态时,你可以通过同步机制,实现原子操作,规避『执行时序』的不确定,从而规避竞态条件,避免线程不安全
C、什么是正确的执行时序?
多个线程,对于共享可变状态的『读取+修改操作』,以串行的方式执行,这样的执行时序就是正确的
D、如何确保正确的执行时序
使用复合操作——让多个线程间,在对某个状态变量进行『读取+修改操作』时,得以通过原子的方式执行,也即是以串行的方式进行,此时执行时序就一定是正确的了
2、复合操作
①严谨地描述复合操作
假定有两个操作A和B,如果从执行A操作的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于『访问同一个状态的所有操作』(包括该操作本身)来说,这个操作是一个以原子方式执行的操作
②什么是复合操作?
包含了一组必须以原子方式执行的操作,从而确保线程安全性。由于『访问同一个状态的所有操作』必须以原子的方式执行,所以『访问同一个状态的所有操作』就必须组合在一起,作为一个复合操作
③如何定义复合操作
A、方法一:加锁机制(下一节讲)
B、方法二:使用一个现有的原子变量类
a、代码
public class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount(){ return count.get(); } public void service(ServletRequest req, ServletResponse resp) { Biglnteger i = extractFromRequest(req) ; Biglnteger [] factors = factor{i) ; count.incrementAndGet(); encodelntoResponse(resp, factors); } }
b、解释
A、原子变量类在什么包中? 在 java.util.concurrent.atomic 包中包含了一些原子变量类 B、原子变量类的作用? 用于实现在数值和对象引用上的『原子状态转换』,也即将『读取-修改-写入』等操作作为一个原子操作 C、作用举例 通过用 AtomicLong 来代替 long 类型的计数器,能够确保所有对计数器状态的访问操作都是原子的(用 count.incrementAndGet() 代替了 count++ ) D、如果成员是线程安全的,那么对象就是状态安全的? 由于 Servlet 的状态就是计数器的状态(因为 servlet 只有计数器对象这一个属性),并且计数器是线程安全的,因此这里的 Servlet 也是线程安全的。
c、结论
结论——使用一个现有的原子变量类(线程安全类)确保原子性 我们在因数分解的 servlet 中增加了一个计数器,并通过使用线程安全类 AtomicLong 来管理计数器的状态,从而确保了代码的线程安全性。 A、当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。 B、在实际情况中,应尽可能地使用现有的线程安全对象(例如AtomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。
第三节 加锁机制
一、原子变量类的局限性
1)当前的原子变量类,使用情况?
当在 Servlet 中添加『一个状态变量』时,可以通过『线程安全的原子变量类实例对象』来管理『 Servlet 的计数器状态』(由于只有一个状态变量,此时 servlet 的状态就是这个对象的状态)以维护 Servlet 的线程安全性
2)当前的原子变量类,存在的问题?
但如果想在 Servlet 中添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?
二、带有缓存的因式分解servlet
1)目的
假设我们希望提升 Servlet 的性能:将最近的计算结果缓存起来,当『两个连续的请求』对『相同的数值』进行因数分解时,可以直接使用上一次的计算结果,而无须重新计算(这并非一种有效的缓存策略,5.6节将给出一种更好的策略)
2)如何实现
1、思路
要实现该缓存策略,需要保存两个状态:最近『执行因数分解』的数值,以及分解结果
3)疑问
A、我们曾通过『线程安全的类』 AtomicLong 以线程安全的方式来管理计数器的状态 B、那么,在这里是否可以使用类似的『线程安全的类』AtomicReferencee,来管理最近执行因数分解的数值及其分解结果