当前位置: 首页 > news >正文

多线程数据共享

多线程之间的数据共享与可见性

如你所知,任何程序都在一个特殊的内存区域中处理数据。每个进程都有自己的内存,但如果我们在该进程中使用多线程,线程就可以访问这片特定区域,从而彼此进行交互。本节将介绍线程如何通过共享数据进行通信,以及开发者在此过程中可能遇到的问题。


线程之间的数据共享

属于同一进程的线程共享公共内存(称为堆内存)。它们可以通过内存中的共享数据进行通信。为了让多个线程访问同一数据,每个线程都必须持有对该数据的引用(例如一个对象)。下图展示了这一思想:

示意图:多个线程共享堆内存中的对象引用。

让我们来看一个示例。以下是一个名为 Counter 的类:

class Counter {var value = 0fun increment() {value++}
}

这个类有一个函数 increment(),每次调用会让 value 字段加 1。

接下来是一个继承自 Thread 的类:

class MyThread(val counter: Counter) : Thread() {override fun run() {counter.increment()}
}

MyThread 构造函数接收一个 Counter 实例,并存储在字段中。run 方法调用 counter.increment()

现在我们创建一个 Counter 实例和两个 MyThread 实例:

val counter = Counter()val thread1 = MyThread(counter)
val thread2 = MyThread(counter)

启动这两个线程并打印 counter.value

thread1.start()  // 启动第一个线程
thread1.join()   // 等待第一个线程完成thread2.start()  // 启动第二个线程
thread2.join()   // 等待第二个线程完成println(counter.value) // 输出结果是 2

结果是 2,因为两个线程操作的是同一个 counter 实例,值被增加了两次。

注意:这个例子中两个线程并不是“同时”操作数据的 —— 第二个线程在第一个线程结束后才开始执行。


线程干扰(Thread Interference)

非原子操作是由多个步骤组成的操作。如果一个线程在另一个线程执行的非原子操作中途介入,就可能产生线程干扰问题 —— 多个线程的操作步骤交错执行,产生意料之外的结果。

Counter 类为例,value++ 实际上由三个步骤组成:

  1. 读取当前值;

  2. 将值加一;

  3. 将结果写回字段。

由于这个操作是非原子的,若两个线程几乎同时调用 increment(),可能会出现如下情况:

  • 线程 A:读取 value(值为 0)

  • 线程 A:加 1

  • 线程 B:读取 value(值仍为 0)

  • 线程 A:写入结果(值变为 1)

  • 线程 B:加 1

  • 线程 B:写入结果(值仍为 1)

最终结果是 1 而不是 2,线程 A 的结果被线程 B 覆盖了。

类似地,value-- 也存在这个问题。


long 与 double 的原子性问题

你可能会惊讶:即便是读取和写入 long(长整型)或 double(双精度浮点)变量,在某些平台上也不是原子操作

class MyClass {var longVal: Long = 0        // 非原子性var doubleVal: Double = 0.0  // 非原子性
}

这意味着一个线程在写入时,另一个线程可能看到的是“写了一半”的中间值(比如只写了前 32 位)。

为了解决这个问题,可以使用 @Volatile 注解:

class MyClass {@Volatile var longVal: Long = 0@Volatile var doubleVal: Double = 0.0
}

这样就保证了对这些变量的读取与写入是原子的。

注意BooleanByteShortIntCharFloat 类型的读取与写入在 JVM 上是原子的,无需加 @Volatile


线程之间的可见性(Visibility)

在并发程序中,一个线程对共享变量的更改,另一个线程可能看不到这些变化,或者看到的是错乱顺序的值,这叫做可见性问题

原因在于:

编译器、运行时和 CPU 为了优化性能,可能进行缓存指令重排序,这些优化会在并发上下文中造成问题。

来看一个示例:

var number = 0
var ready = falseclass Reader : Thread() {override fun run() {while (!ready) {yield() // 暂时释放资源}println(number)}
}fun main() {Reader().start()number = 42ready = true
}

按理说输出应为 42,但实际上也可能输出 0。这是因为:

  • 主线程对 numberready 的修改可能被缓存在寄存器或写缓冲区中;

  • Reader 线程可能读取到旧值(0)或乱序执行后的值。

为了解决这个问题,我们可以使用 @Volatile

@Volatile var number = 0
@Volatile var ready = false

@Volatile 保证对该字段的更改对所有线程都是立即可见的,并避免了重排序带来的问题。


可见性无需 @Volatile 的两种情况

某些情况下,不使用 @Volatile 也可以保证可见性:

  1. 在线程启动前,对变量的修改对新线程是可见的。

  2. join() 返回之后,该线程内部对变量的修改对其他线程是可见的。


小结

  • 属于同一进程的线程通过堆内存共享数据。

  • 非原子操作的步骤可能交错执行,造成 线程干扰(Thread Interference)

  • @Volatile 注解保证对变量的修改对所有线程立即可见,并使读取和写入操作具备原子性(不包括递增或递减操作)。

  • @Volatile 不保证复合操作(如 value++)的原子性。

  • 可见性问题在多线程程序中是常见而又隐蔽的 bug 来源。

http://www.dtcms.com/a/297561.html

相关文章:

  • adb 下载并安装
  • 中国高精度绿洲数据集
  • 基于华为openEuler系统部署NFS文件共享服务
  • 开疆智能ModbusTCP转Profient网关连接西门子PLC与川崎机器人配置案例
  • ModelWhale+数据分析 消费者行为数据分析实战
  • UE5多人MOBA+GAS 30、技能升级机制
  • 计算机体系结构中的中断服务程序ISR是什么?
  • Android 的16 KB内存页设备需要硬件支持吗,还是只需要手机升级到Android15系统就可以
  • Haproxy七层代理及配置
  • LabVIEW VI 脚本:已知与未知对象引用获取
  • 在 .NET 中使用 Base64 时容易踩的坑总结
  • iOS 日志查看实战指南,如何全面获取与分析 App 和系统日志
  • 栈与队列:数据结构核心解密
  • CurseForge中文官网 - 我的世界游戏MOD模组资源下载网站|下载入口|打不开
  • AMBA - CHI(2) 基本结构和对应通道信息
  • 基于深度学习的胸部 X 光图像肺炎分类系统(五)
  • 【Linux】进程切换与优先级
  • Mysql 索引下推(Index Condition Pushdown, ICP)详解
  • RK3588 HDMI-RX 驱动、RGA 加速与 OpenCV GStreamer 支持完整指南
  • 测试覆盖率:衡量测试的充分性和完整性
  • 巧用Proxy与异步编程:绕过浏览器安全限制实现文件选择器触发
  • JAVA同城服务家政服务家政派单系统源码微信小程序+微信公众号+APP+H5
  • 大语言模型生成式人工智能企业应用
  • 【Android】桌面小组件开发
  • 【通识】如何看电路图
  • Python 程序设计讲义(21):循环结构——while循环
  • C++ 常用的数据结构(适配器容量:栈、队列、优先队列)
  • centos 7 开启80,443端口,怎么弄?
  • CentOS 8 安装HGDB V4.5 psql命令执行报错
  • VR 污水处理技术赋能广州猎德污水处理厂,处理效率显著提升