【程序员的必修课】并发编程--理论基础

本文介绍了并发编程的基础知识,包括为什么需要多线程、并发问题的根源及其解决方法。主要探讨了可见性、原子性和有序性问题,以及Java如何通过JMM、关键字如volatile和synchronized来解决这些问题。此外,文章还提到了线程安全的实现方法,如互斥编程、非阻塞编程和无同步方案。最后,总结了并发编程中的一些关键概念和重要知识点。
摘要由CSDN通过智能技术生成

并发编程-理论基础

一、前言

很多小伙伴学习并发编程,上来就是看 JUC 包,背面试题,但是对并发编程的底层原理不甚了解,导致写出的程序出现奇怪的问题,也没有足够的理论支撑去排查,说实话,我一开始也是这样的。但是随着学习的深入和面试的经历,我越发了解到系统学习并发编程的重要性。

从今天开始,我就带领小伙伴们,从理论触发,逐渐吃透并发编程

俗话说得好,基础不来,地动山摇,在学习并发编程之前,我们要先了解并发编程解决的原因、其引发非问题

这里要提醒一句,并发编程和 JMM (内存模型)密不可分,小伙伴在学习之前,可以先去复习一下 JMM 的知识哦

二、理论基础

1、为什么需要多线程

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

  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异(在耗时的 IO 执行的时候,可以切换线程执行其他任务,这是多线程出现的最重要的原因

这种方式导致原子性问题

  • CPU 增加了缓存,以均衡与内存的速度差异(CPU 寄存器,一级缓存,二级缓存…其均衡与内存的差异的主要做法,是先将修改写入缓存中,然后在一定时间后,才会将缓存内容写入内存,这样,可以将多次对内存的修改,打包成一次进行修改,减少对内存总线的占用)

导致 可见性问题

  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用

导致有序性问题

2、并发问题的根源

可见性、原子性、有序性

1)可见性

可见性,是由于 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修改的值

可见性问题

2)原子性

数据库里,我们有 ACID 四个事务的性质,其中 Atomicity,及原子性,它表示的意思是对于同一个事务,在提交之前,其内部的操作要么全部成功,要么全部失败

并发编程中的原子性问题,其实和这个差不多,比如说我们希望一个线程,可以处理几个任务,而不被其他线程打扰

**经典的问题:**写一个方法,模拟账户A 向账户B 转账

最最简单的想法,是这样的:

public boolean transform(Account a,Account b) {
   
  synchronized(a) {
   
    synchronized(b) {
   
      // 执行转账操作
    }
  }
}

这个写法,有一个致命的问题,就是有可能发生死锁。假如在线程 a 想向线程b 转钱的过程中,线程b 又向线程a 转钱,就可能出现 a 在等待获取资源b,b 在等待获取资源 a 的尴尬局面

有小伙伴可能会说了:"那么我们给方法加一个 synchronized 锁不就好了"

public synchronized boolean transform(Account a,Account b) {
   
  // 执行转账操作
}

这样的操作也有自己的问题,就是其效率是在是太低了,为方法加上 synchronized 锁,那么同一时间内,一次只能有两个对象进行操作,其他的转账操作全部都要排队。假设像支付宝这种级别的支付业务,一次支付可能要排队好几个小时

通过上面两个例子,我们就可以清楚的认识到,并发编程的原子性,要考虑的不是怎么实现原子性,而是怎么保证高效和不会死锁,这些在(包括我们上面举的银行转账的例子),我们会在讲 JUC 的时候,再次提及

3)有序性

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序

重排序后出现问题的例子如下:

// Processor A
a = 1; //A1  
x = b; //A2

// Processor B
b = 2; //B1  
y = a; //B2

// 初始状态:a = b = 0;处理器允许执行后得到结果:x = y = 0

这段代码,因为重排序的存在,执行结果可能为 x = y = 0

那为什么需要重排序?重排序提高效率又体现在哪里?

通过下图可以发现,经过重排序后,会先将对数据写后的结果,写入缓冲区中,接着,不是按照顺序去刷新主存,而是先去读取主存中的数据,之所以会出现这么荒唐的结果,是因为处理器希望所有缓存对主存的操作一次完成,从而提高效率(IO 永远是程序运行效率的最大瓶颈),而这也就造成了我们程序员的麻烦

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FARO_Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值