ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

【并发编程理论】1.并发问题的由来

2020-07-07 23:37:27  阅读:224  来源: 互联网

标签:缓存 instance 理论 编程 并发 线程 内存 操作 CPU


并发编程中问题的由来:

CPU、内存、I/O设备的速度存在巨大差异,程序的整体性能取决于最慢的操作——读取I/O设备,为了合理利用CPU性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做出了以下改进。

  1. CPU增加了缓存
  2. 操作系统增加进程、线程分时复用CPU,进而均衡CPU与I/O设备的速度差异
  3. 编译程序优化指令执行次序,使得缓存能够更加合理地利用
1.可见性——CPU缓存导致

早期单核CPU时,CPU缓存的数据与内存的数据是保持一致的,
线程A,线程B在同一个核心上切换运行,A对缓存中的操作对于B是立即可见的,所以不存在可见性问题

到了多核CPU时代,每个核心都有自己的CPU缓存(L1 cache,L2 cache).
线程A,线程B在执行不同的核心上执行时,先将数据从内存读取到各自的CPU缓存,此时线程A对于数据的操作,对于线程B是不可见的。

写操作对于读操作 是立即可见的

2.原子性——线程切换导致

高级程序语言中的一条语句往往对应这操作系统中的多条指令,
线程在CPU的执行又是时间片轮转的方式执行,和可能在线程A将内存中的变量值V = 0 读取到寄存器中时,切换到线程B执行将V的值进行了更新 V=V+1
此时V = 0 更新到内存,切换到A线程,对寄存器中 V=0 进行V++,在更新到内存 V=1。
就会出现两个线程进行 V = V+1操作,结果却还是2的情况。

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称 为“原子性”

3.有序性——编译器优化导致

编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”
编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,
但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。

例如:双重校验单例模式

public class Singleton {
    static Singleton instance;
    static Singleton getInstance(){
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

理想的执行过程:
假设线程A、线程B同时调用getInstance()方法,

  • 1.假设A先得到执行权对Singleton.class进行加锁
  • 2.此时instance = null 创建Singleton对象
  • 3.获取到单例对象,释放锁
  • 4.线程B调用getInstance(),instance不为null,获取到线程A初始化的instance

经过编译优化后对象的创建过程

  • 1.分配一块内存 M;
  • 2.将 M 的地址赋值给 instance 变量;
  • 3.最后在内存 M 上初始化 Singleton 对象。

当执行2后发生线程切换,线程B得到的instance应用地址并未初始化对象就会产生空指针。

Java内存模型如何解决并发问题

Java内存模型针对于
可见性问题 ——CPU缓存导致
有序性问题 ——编译优化导致
原子性问题 ——线程切换导致
提供的方案是按需禁用CPU缓存及编译优化
https://www.bilibili.com/video/av81008349

具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及Happens-Before 规则

volatile关键字:
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
虽然禁用了缓存,所有线程都要从内存中读取,但是不具备互斥性(即同一时间只有一个线程可以执行)
适用于一个线程写 另一个线程读 可以读到写入的最新值

synchronized关键字:
同步关键字,进行修饰的代码块或者方法,进行加锁,保证同时只有一个线程能够获取到锁。

Happens-Before 约束 了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 HappensBefore 规则。
核心原则:前面一个操作的结果对后续操作是可见的。
六大规则:

  • 1.程序的顺序性规则
    指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意 操作。
    即程序前面对某个变量的修改一定是对后续操作可见的。
  • 2.volatile 变量规则
    指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
  • 3.传递性
    指如果 A Happens-Before B,且 B Happens-Before C,那么 A HappensBefore C。
  • 4.管程中锁的规则
    管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
    上一个加锁的线程中的操作释放锁后对下一个加锁线程可见。
  • 5.线程 start() 规则
    指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在 启动子线程 B 前的操作。
  • 6.线程 join() 规则
    指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能 够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。

volatile 保证可见性,一个线程的写对另一个线程的读可见 无锁
AtomicInteger(原子类) 保证原子性,同时进行读写时的操作是原子性的. (基于CPU提供的CAS) 无锁
Synchrnoized 最为重量级 同步代码块是原子性的,同时只有一个线程可以执行 加锁

标签:缓存,instance,理论,编程,并发,线程,内存,操作,CPU
来源: https://www.cnblogs.com/shinyrou/p/13264240.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有