线程安全性

线程安全性概念:

多个线程访问某个类,该类始终表现出正确的行为,该类线程安全

  • 正确性:某个类的行为和规范完全一致
  • 示例:大多数Servlet,无状态的:不包含域,不包含对其他类中域的引用,线程访问无状态对象的行为并不会影响其他线程中操作的正确性

原子性

  • 如果在Servlet中增加域,则并非线程安全。如count++这种操作,会造成数据偏差
  • 读取-修改-写入

竞态条件

由于不恰当的执行顺序导致的出现的不正确的结果

  • 类型:先检查后执行,基于某个可能失效的观察结果来做出判断或者执行某个计算

常见类型:延迟初始化

返回不同实例会导致丢失信息等问题

复合操作

  • 复合操作原子化,可以确保线程安全性
  • 利用原子变量类,无状态类中添加一个状态时,完全有线程安全的对象来管理,该类仍然是线程安全的

加锁机制

如果添加更多的状态机制,如何保证线程安全?

  • 如果只考虑原子类的话,一些不变形条件就会被破坏从而导致线程不安全
  • 要保持状态的一致性,需要在单个原子操作中更新所有相关的状态变量

内置锁

  • synchronized,同步代码块,锁住对象或者类(内置锁)
  • 互斥锁(可能会导致服务响应很低)

重入

  • 避免了死锁情况的发生,线程试图获得由它自己持有的锁,可以成功

用锁来保护状态

  • 如果用同步协调对变量的访问,访问这个变量的所有位置都需要使用同步
  • 如果用锁来协调某个变量的访问,在访问变量的所有位置都要使用同一个锁
  • 当获取与对象关联的锁时,并不能阻止其他线程访问该对象
  • 需要自行构造加锁协议或者同步策略实现对共享状态的安全访问,并在程序中一直使用它们
  • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要同一个锁来保护

活跃性和性能

  • 应尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去
  • 使用两种不同的同步机制不仅会带来混乱,也不会在性能和安全上带来任何好处
  • 获取和释放锁也需要开销,因此如果代码块分解的太细,通常不好
  • 不要盲目的为了性能牺牲简单性(会破坏安全性)
  • 执行较长时间的计算或者无法快速完成的操作时(网络io或者控制台io),不要持有锁

对象的共享

可见性

  • 为了保证多个线程之间对内存的写入操作是可见的,必须使用同步机制
  • 重排序问题:无同步情况下,编译器、处理器以及运行时等都可能都对操作的执行顺序进行一些意想不到的调整

失效数据

  • 未经同步的程序可能会访问失效数据,会导致危险的情况
  • GuardedBy注解 synchronized

非原子的64位操作

  • 最低安全性:失效值时之前某个线程设置的值
  • 非volatile类型的64为数值变量被JVM允许分解为两个32位的操作,不安全

加锁与可见性

  • 线程B执行由锁保护的代码块时,可以看到线程A之前在同一个代码块中的所有操作结果
  • 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步

volatile变量

  • 编译器与运行时都会注意到这个变量是共享的,不会将该变量上的操作与其他内存操作一起重排序
  • 比synchronized更轻量的同步机制
  • 仅当volatile能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,就不要使用volatile变量。
  • 确保自身状态可见性;确保引用对象状态可见性;标识一些重要的程序生命周期事件的发生
  • 加锁机制既可以确保可见性又可以确保原子性,volatile只确保可见性
  • 使用:对变量的写入操作不依赖变量的当前值,不会与其他状态变量一起纳入不变性条件,访问变量不需要加锁

发布与逸出

  • 发布:使对象能够在当前作用域之外的代码中使用
  • 逸出:某个不该被发布的对象被发布
安全的对象构造过程
  • 不要在构造过程中让this引用逸出
  • 使用工厂方法来防止this引用在构造过程中逸出

线程封闭

  • 仅在单线程内访问数据
  • 例子:Swing和JDBC的Connnection对象

Ad-hoc线程封闭

  • 线程封闭完全由程序实现来承担
  • 具有脆弱性,应减少使用

栈封闭

  • 线程封闭的一种特例
  • 只能通过局部变量才能访问对象

ThreadLocal

  • 线程中的某个值以及保存该值的对象联系起来
  • 特定于线程的值保存在Thread对象中,线程终止后,这些值会作为垃圾回收
  • 类似于全局变量,降低代码的可重用性并在类之间引入隐含的耦合性,使用时需格外小心

不变性

  • 不可变对象一定是线程安全的
  • 满足条件:创建后状态不能修改;所有域都是final类型;对象是正确创建的(this引用没有逸出)

final域

  • final类型的域是不可更改的;能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无须同步
  • 通过将可变对象的某些域声明为final,仍然可以简化对状态的判断
  • 将变量存在不可变对象中来消除多个相关变量出现时的竞态条件
  • 使用volatile类型的引用确保可见性,使用包含多个状态变量的容器对象来维持不变性条件

安全发布

  • 不正确的发布:正确的对象会被破坏
  • 任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使发布这些对象时没有使用同步
  • 常用模式:在静态初始函数中初始化一个对象引用;将对象的引用保存到volatile类型的域或者AtomicReference对象当中;将对象的引用保存到某个正确构造对象的final类型域中;对象的引用保存在一个由锁保护的域中
  • 事实不可变对象:技术上是可变的,状态在发布后不会发生改变
  • 可变对象:只能保证发布时的状态可见性
  • 对象的发布需求取决于他的可变性:不可变对象通过任意机制来发布;事实不可变对象必须通过安全方式来发布;可变对象必须通过安全方式来发布,且必须是线程安全的或者由某个锁保护起来
  • 安全的共享对象

使用和共享对象策略

  • 线程封闭:只能由一个线程拥有,对象被封闭在该线程中,只能由该线程修改
  • 只读共享:共享的只读对象不能被修改,可以被多个线程访问(包括不可变对象和事实不可变对象)
  • 线程安全共享:内部实现同步,多个线程可以通过对象的公有接口进行访问
  • 保护对象:需要持有特定的锁来访问。包括封装在其他线程安全对象中的对象,以及一发布的并且由某个特定锁保护的对象

对象的组合

设计线程安全的类

  • 设计线程安全类的基本三要素:找出构成对象状态的所有变量,找出约束状态变量的不变性条件,建立对象状态的并发访问管理策略
posted on 2023-05-05 23:04  JohnWangzx  阅读(40)  评论(0)    收藏  举报