type
status
date
slug
summary
tags
category
icon
password
Property
Jul 18, 2023 02:08 AM
本文为铺垫文章

JUC-Java并发编程

notion image
 

一、逆向看待并发编程

为什么需要并发编程?

 
CPU与内存读写数据的速度是不同的,CPU的读写速度通常比内存快得多。这是因为CPU的寄存器缓存通常都比内存更快,而且CPU可以利用多级缓存来加速数据访问。而内存的读写速度受到多种因素的制约,包括内存类型内存带宽内存容量等。
在计算机系统中,数据通常需要从内存中读取到CPU的寄存器或缓存中进行处理,然后再将结果写回内存中。这个过程中,如果CPU可以利用缓存来提高数据访问速度,那么整个处理过程会更快。另外,现代CPU还支持一些高级功能,如指令乱序执行、分支预测等,这些功能可以进一步提高CPU的处理效率。因此,尽管CPU和内存的读写速度不同,但在实际应用中,系统设计者通常会尽可能利用它们各自的优势,以达到最佳的性能表现。
以下内容来自@Pdai,下段文字著作权归@pdai所有
CPU、内存、I/O 设备的速度是有极大差异的
为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
  • CPU 增加了缓存,以均衡与内存的速度差异;从而间接导致了 可见性 的问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;导致了 原子性 的问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。 导致了 有序性 的问题

线程不安全示例

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。

# 并发出现问题的根源: 并发三要素

上述代码输出为什么不是1000? 并发出现问题的根源是什么?

# 可见性: CPU缓存引起

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
举个简单的例子,看下面这段代码:
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

# 原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个简单的例子,看下面这段代码:
这里需要注意的是:i += 1需要三条 CPU 指令
  1. 将变量 i 从内存读取到 CPU寄存器;
  1. 在CPU寄存器中执行 i + 1 操作;
  1. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

# 有序性: 重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
notion image
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)
 
所以,JAVA是怎么解决并发问题的: 请看下一篇:JMM(Java内存模型)剖析
 
💡
有关多线程并发编程知识体系上的问题,欢迎您在底部评论区留言,一起交流~
 
 
JDK-Tips(一):关于Lambda的巧妙使用LeetCode刷题日记(1):今日刷题-基础数组双指针-034
fntp
fntp
多一点兴趣,少一点功利
公告
type
status
date
slug
summary
tags
category
icon
password
Property
Sep 5, 2023 06:04 AM
📝 博客只为了记录我的学习生涯
😎 我的学习目标是成为一名极客
🤖 我热爱开源当然我也拥抱开源
💌 我期待能收到你的Email留言
📧 我的邮箱:stickpoint@163.com
欢迎交流~