为什么要出现并发?并发的三要素

发布时间:2023年12月25日

大家好,我是"java继父"伯约,假如这篇对大家有帮助的话求一个赞,另外文章末尾放了我从小白到架构师多年的学习资料。

1.为什么需要多线程

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

2.线程不安全示例

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。

public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
997 // 结果总是小于1000

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

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

可见性: CPU缓存引起

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程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修改的值。

原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

举个简单的例子,看下面这段代码:

int i = 1;

// 线程1执行
i += 1;

// 线程2执行
i += 1;

这里需要注意的是:i += 1需要三条 CPU 指令

  1. 将变量 i 从内存读取到 CPU寄存器;
  2. 在CPU寄存器中执行 i + 1 操作;
  3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

有序性: 重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

????????上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

4.粉丝福利

最新很多同学问我有没有java学习资料,我根据我从小白到架构师多年的学习经验整理出来了一份50W字面试解析文档、简历模板、学习路线图、java必看学习书籍、需要的小伙伴可以关注我的
公众号:“Tom聊架构”,回复暗号:“578”即可获取

文章来源:https://blog.csdn.net/weixin_46781683/article/details/135194688
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。