Files
Hui-s-notebook/线程安全.md
2023-09-10 10:50:53 +08:00

4.4 KiB
Raw Permalink Blame History

定义

当多个线程同时访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象都可以获得正确的结果, 那就称这个线程是安全的。

所有线程都必须具备一个共同的特征: 代码本身封装了所有必要的正确性保障手段 (如互斥同步等) 令调用者无序关心多线程下的调用问题, 更无需自己实现任何措施来保证多线程下的正确调用

Java 语言中的线程安全

  1. 不可变

Java 里面,不可变的对象一定是线程安全的, 只要一个不可变的对象被正确构造出来,那其外部的可见状态永远不会改变

Java 类库 API 中不符合可变要求的类型: String、 枚举类型、 Java. Lang. Number 部分子类,如 Long 和 Double、BigInteger、BigDecimal

  1. 绝对线程安全

Java API 标注线程安全的类,绝大多数都不是绝对线程安全

  1. 相对线程安全

通常意义上所讲的线程安全, 需要保证对这个对象单次操作是线程安全的,调用时无需额外的保证措施

  1. 线程兼容

对象本身并不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发环境中可以安全使用

  1. 线程对立

不管调用端是否使用了同步措施,都无法在多线程环境中使用并发代码

线程安全的实现方法

  1. 互斥同步

最常见也最主要的并发正确性保证手段, 多个线程并发访问数据时,保证共享数据在同一时刻只被一条线程使用。

最基本的互斥同步手段是 synchronized 关键字,使用时要注意:

  • 被修饰的同步块对同一线程是可重入的, 同一线程反复进入不会把自己锁死
  • 被修饰的同步块在持有锁的线程执行完毕并释放锁之前, 会无条件阻塞后面其它线程的进入, 无法像处理某些数据库中的锁一样, 强制已获取锁的线程释放锁, 也无法强制正在等待锁的线程中断等待或超时退出

Lock 接口可以以非块结构实现互斥同步,可在类库层面实现同步

重入锁是 Lock 接口的最常见一种实现, ReentrantLock 在 synchronized 基础上添加了一些高级功能:

  • 等待可中断: 当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 该为处理其它事情
  • 公平锁: 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
  • 锁绑定多个条件: 一个 ReentrantLock 对象可以绑定多个 Condition 对象

优先使用 Synchronized 情况:

  • 只需要基础同步功能时
  • Lock 应该确保在 finally 块中释放锁, 否则一旦受同步保护的代码块中抛出异常, 则有可能永远不会释放持有的锁, 使用 Synchronized 可以由虚拟机确保即时出现异常锁也能自动释放
  • Java 虚拟机更容易针对 Synchronized 优化, 可以在线程中获取锁的相关信息, 使用 Lock 虚拟机很难得知具体哪些锁是哪些对象持有的
  1. 非阻塞同步

基于冲突检测的乐观并发策略, 不管风险,先进行操作, 如果没有其他线程争用共享数据, 那操作就成功了, 如果共享的数据被争用,产生了冲突, 那在进行其它补偿措施, 最常用的补偿措施是不断重试,直到出现没有竞争的共享数据

乐观并发策略需要"硬件指令集的发展",硬件指令有:

  • 测试并设置 (Test-and-Set)
  • 获取并增加 (Fetch-and-Increment)
  • 交换 (Swap)
  • 交换并比较(Compare-and-SwapCAS)
  • 加载链接/条件储存(Load-Linked/Store-ConditionalLL/SC)

CAS 操作的 “ABA 问题

  1. 无同步方案

让一个方法不涉及共享数据,自然就不需要任何同步措施保证正确性

可重入代码 又称纯代码, 指在代码执行的任何时段中断,去执行另外一段代码, 控制权返回后, 原来的程序不会出现任何错误,也不会对结果有影响

特征: 不依赖全局变量、 存储在堆上的数据、 公用的系统资源

线程本地存储 如果一段代码中所需要的数据必须与其他代码共享, 那就看看这些共享数据的代码是否能保证在同一个线程中执行