并发编程之原子性、有序性及可见性。

java 同时被 3 个专栏收录
13 篇文章 0 订阅
15 篇文章 0 订阅
9 篇文章 0 订阅

问题引入

在计算机执行程序的过程中,每条指令都是在cpu中执行的。程序中的临时数据都是存放在主存中的。而cpu和数据直接产生交互的是高速缓存。

在程序运行过程中,会将运算需要的数据从主存拷贝一份到高速缓存中。
那么cup在进行计算时直接可以从高速缓存中读取数据和写入数据,运算结束后,再将高速缓存中的数据刷新到主存。

很典型的一个例子:

	i = i+1;

当线程执行这个语句时,cpu首先会先从主存中读取i的值,然后复制到高速缓存,然后cpu会对i进行+1的操作,然后写入高速缓存,最后刷新到主存中。

缓存一致性问题

这个在单线程执行时看似是没有问题的,但是在多线程的情况下,就会出现缓存不一致的问题。

比如有两个线程,一开始的时候,每个线程会将读取的i复制到自己的高速缓存中,线程1 对i进行了+1的操作,然后把i写入到内存,这时线程2中的高速缓存里i仍然为0,同时也对i进行了+1的操作,然后写入内存,这时最终的结果仍为1。我们期望的结果是2,但是最终结果为1 ,这种情况就是著名的CPU缓存一致性问题。也通常称之为多个线程访问的变量为共享变量。这种情况一般出现在多线程编程。

如何解决缓存一致性问题?

为了解决缓存一致性,通常有两种解决方案。

  1. 总线锁机制
  2. 缓存一致性协议

这两者都是硬件层面的解决方式

总线锁机制

早期cpu是通过对总线加锁进行处理的,因为cpu与其他组件进行通信都是通过总线进行的。如果对总线加锁,就会阻塞了其他cpu对组件的访问,从而使得只有一个cpu能使用这个变量的内存。
但是这种方式有一个很大的缺陷,就是当在对总线加锁期间,其他cpu都不能访问内存,从而导致效率低下。

缓存一致性协议

于是后来出现了一种缓存一致性协议,最出名的是intel的MESI协议,它保证每个线程的共享变量的副本是一致的。

它的核心思想是,当CPU写数据时,如果发现操作的是共享变量,即其他cpu也存在该变量的副本,会发出信号通知其他cpu将该变量的缓存行置为无效状态。因此其他cpu当读到这个变量是无效状态时,那么他就会从内存重新读取。

并发编程中三个核心概念

  1. 原子性
  2. 有序性
  3. 可见性

原子性

程序执行一个或者多个操作,要么同时执行成功,要么同时执行失败。

有序性

程序的执行顺序要按照代码的先后顺序执行

可见性

当多线程访问同一个变量时,一个线程修改了变量的值,对于其他线程是可见的。

其他两个大家可能都理解,重点说一下有序性:

当代码中的数据没有依赖性的时候,处理器为了提高性能,可能会对代码进行优化,他不会保证程序中各个语句的执行先后顺序和代码里的一致,但他会保证最终的结果一致。
比如:

int i = 9;  //(1)
int b = 1;   //(2)

int c = i+b;    //(3)

其中(1)和(2)中的代码数据没有依赖,但是(3)用到了前两行的结果,所以(3)不会重排序,(1)和(2)的执行顺序不能保证。

这种情况在单线程的情况是没有问题的,但是在多线程的情况就会出现问题。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep() 
}
doSomethingwithconfig(context);

因为线程1中的语句1和语句2是没有依赖性的,所以语句1和语句2可能发生重排序。
如果线程1先执行语句2,此时线程2执行while中条件为false,会以为线程1的context初始化完成,执行doSomethingwithconfig(context),这时线程2就会报错。

由此可见,指令的重排序在单线程情况是没有问题的,但是在多线程的情况就可能出现程序的错误。

也就是说,要想保证多线程程序能够正确的执行,必须要保证程序的原子性,可见性以及有序性。如果有一个不能被保证,就可能会出现程序运行不正确。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值