面试准备每日系列:计算机底层之并发编程(一)原子性、atomic、CAS、ABA、可见性、有序性、指令重排、volatile、内存屏障、缓存一致性、四核八线程
文章目录
- 1. 什么是进程?什么是线程?
- 2. 线程切换
- 3. 四核八线程是什么意思
- 3.1 单核CPU设定多线程是否有意义
- 4. 并发编程的原子性
- 4.1 如何解决原子性问题 & atomic(底层CAS)
- 5. 并发编程的可见性
- 5.1 如何解决可见性问题
- 6. 并发编程的有序性 & 指令重排
- 6.1 如何解决有序性问题
- 7. volatile底层
- 7.1 无法保证原子性
- 7.2 保证可见性
- 7.3 保证有序性
- 8. 单例设计模式中的volatile
- 9. 对象创建时启动线程的指令重排现象
- 10. 既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
- 总结
并发编程三大特性:
- 可见性
- 有序性
- 原子性
1. 什么是进程?什么是线程?
进程:资源分配(静态)
线程:任务调度(动态)
2. 线程切换
Contact Switch
CPU 执行 T1 时,把 T1 的指令和数据放到CPU里,如果需要切换线程了,就把 T1 存在寄存器里的数据和PC 里面的地址 放到 cache 里面,然后就把 T2 的指令和数据放进来;
3. 四核八线程是什么意思
CPU
里面有ALU
和 Registers
,ALU
负责运算,Registers
存取数据;
一个 ALU
相当于一个核;一组寄存器可以存放一个线程的数据;所以,当一个ALU
能够对应两组寄存器,那么就是四核八线程!超线程亦是如此;
3.1 单核CPU设定多线程是否有意义
单核CPU设定多线程,当一个线程等待资源分配或者别的事件发生时,CPU可以运行别的线程,提高效率;这和分时操作系统并发运行多个进程的本质一样;
4. 并发编程的原子性
原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行;
比如 i=1 是原子操作,只涉及赋值;而 i++ 就不是原子操作,它相当于语句i=i+1;这里包括读取i,i+1,结果写入内存三个操作单元;
并发编程中,因此如果操作不符合原子性操作,那么整个语句的执行就会出现混乱,导致出现错误的结果,从而导致线程安全问题;
4.1 如何解决原子性问题 & atomic(底层CAS)
如何保证操作的原子性呢?
- 加锁,这可以保证线程的原子性,比如使用 synchronized 代码块保证线程的同步,从而保证多线程的原子性。但是加锁的话,就会使开销比较大;
- 使用J.U.C下的 atomic 来实现原子操作;
atomic 的底层是CAS(compareAndSwap),是一条 CPU 并发原语;CAS 并不是一种实际的锁,它仅仅是实现乐观锁的一种思想,java 中的乐观锁(如自旋锁)基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
CAS 算法,即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 CAS 算法涉及到三个操作数:
- 需要读写的内存值 V
- 预期原值 A
- 拟写入的新值 B
如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置值V更新为新值B。否则,处理器不做任何操作。无论哪种情况,它都会返回该位置的【值】;(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了"我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。"
基于 CAS 的并发算法称为无阻塞算法,因为线程不必再等待阻塞。无论 CAS 操作成功还是失败,在任何一种情况中,它都在可预知的时间内完成。如果 CAS 失败,调用者不会被挂起而是可以重试(轮询) CAS 操作或采取其他适合的操作。
CAS 优点:
- synchronized涉及线程之间的切换,存在用户状态和内核状态的切换,耗费巨大。CAS只是CPU的一条原语,是一个原子操作,消耗较少;
缺点:
- ABA 问题;
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A ,变成了 B ,又变成了 A ,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A - 2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference 来解决 ABA 问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
AtomicStampedReference相关方法:
// 比较设置 参数依次为:期望值 写入新值 期望版本戳 新版本戳
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
// 获得当前对象引用
public V getReference()
// 获得当前 版本戳
public int getStamp()
// 设置当前对象引用 和 版本戳
public void set(V newReference, int newStamp)
- 循环时间长开销大;
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销; - 只能保证一个共享变量的原子操作;
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作;从 Java1.5 开始JDK提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作;
数据库中使用 CAS 场景
update user set balance = ${newBalance} where uid = #{uid} and balance = ${oldBalance}
参考文章:
CAS-比较并交换
5. 并发编程的可见性
问题描述:对于这一段程序,我的线程要做的事情是:1、打印m start
;2、只要外部变量running
为true
,我就会陷入死循环,永远也输出不了 m end!
;我的主线程要做的事:1、创建一个线程并让他启动该方法;2、然后让它睡一秒钟;3、置running
为false
,观察是否会输出m end
!
结果是没输出m end
!
为什么会这样,原因就是线程的可见性问题;
可见性:简单的说就是,两个线程同时访问一块内存,当一个线程对它修改之后,另一个线程是不是马上可见;
寄存器从缓存开始一层一层往外找目标数据,如果一直找不到,那就只能在内存中得到数据,然后一层一层往里面放;由于局部性原理,他不会只读单一的目标数据,而是会读临近的一大块数据!一般一次读64
字节,全部读进去;为什么不是32
、128
呢,工程实践的结果,32
太少、128
太多,效率都不及64
字节!
5.1 如何解决可见性问题
实现同步的最少需要两个步骤:主线程中的 running
要先写回到内存,然后通知线程“要它去读内存中的数据”;
这里如果把 system.out.println("hello")
解掉注释,那么就会打印很多 hello
然后出循环 输出"m end!",因为printlf
包含刷新缓存的业务逻辑!就是 printlf
会触发同步;
volatile
能更好地解决可见性问题,被 volatile
修饰的变量,只要发生修改,及时同步,马上其它用到这个值的地方就会进行刷新,拿到最新值!保持线程的可见性;
所以,要想解决可见性问题:
- 要么触发同步指令;
- 要么用 volatile 实现;
6. 并发编程的有序性 & 指令重排
程序真的是按"顺序"执行的吗?
输出结果:
发现同一线程里按顺序写的两条语句,也会发生乱序的情况!
CPU级别,指令是可以重排序的——指令重排;
比如我指令1:x++
;指令2:y=1
;这两条指令完全可以乱序从而提高效率;而x++
和x=1
却不能乱序;
指令重排的条件:
- as-if-serial (看上去像序列执行),即重排序操作不会对存在数据依赖关系的操作进行重排序;
- 不影响单线程的最终一致性;
多线程情况下,指令重排就会出现各种问题;
下面这个案例:
最后结果可能输出0、42
;
两个问题
- 可见性问题:主线程修改
ready
之后 不一定能马上同步到线程 t ; - 有序性问题:在主线程中
ready
和number
互不影响,所以对number
复制的语句和 对ready
赋值的语句可能互换;要是先执行ready=true
,那么就可能在执行number=42
之前先输出number
的值;
6.1 如何解决有序性问题
volatile
指的那块内存,就只能顺序对那块内存进行读写,就是对volatile
修饰的内存的读写不能换顺序!
如何阻止乱序?
volatile
(JVM级别)修饰- 内存屏障
7. volatile底层
7.1 无法保证原子性
java内存模型中定义了8中操作都是原子的,不可再分的:
lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
volatile 保证可见性 是针对原子操作的, a=1是原子性,而a++实际上是a=a+1 是非原子性的,所以会导致上述情况,这时候就要引入同步,强制将a++转化为原子性;同步除了synchronized还 Atomic;
synchronized 看作重量级的锁,而 volatile 看作轻量级的锁
7.2 保证可见性
加了 volatile
的变量,在JVM编译成class
文件时,采用的lock:addl
机制;就是 CPU 是通过总线访问内存的,当一个CPU访问 volatile
修饰的那块内存时,会对总线上锁,这样别的 cpu 就无法访问那段内存;现在的CPU一般会先尝试对缓存行上锁,提高效率、降低开销,待该缓存行更新数据后,进行同步,才释放锁;
这句指令中的 addl $ 0x0, (%esp)
(把 ESP 寄存器的值加 0)是一个空操作(采用这个空操作而不是空操作指令 nop 是因为 IA32 手册规定 lock 前缀不允许配合 nop 指令使用),关键在于 lock 前缀,它的作用是1. 使得本 CPU 的缓存行数据写回到系统内存;2. 这个写回内存的操作也会使其它 CPU 里缓存了该内存地址的数据无效化(Invalidate);
所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里;
Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性;
7.3 保证有序性
加了 volatile 修饰的共享变量,则通过内存屏障解决了多线程下有序性问题;
lock addl $0x0,(%esp)
这个操作相当于一个内存屏障(Memory Barrier 或 Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个 CPU 访问内存时,并不需要内存屏障;但如果有两个或更多 CPU 访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了;
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,下面是基于保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
volatile在写操作前后插入了内存屏障后生成的指令序列示意图如下:
volatile在读操作后面插入了内存屏障后生成的指令序列示意图如下:
lock addl $0x0, (%esp)
指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了指令重排序无法越过内存屏障的效果;
参考文章:
深入理解JVM读书笔记五: Java内存模型与Volatile关键字
面试官:VOLATILE是如何保证可见性和有序性的?
8. 单例设计模式中的volatile
单例设计模式就是,有一个类,你在new
这个类时 必须保证只有唯一的一个实例 only one
,就是不管你new
多少次,都要保证同一个实例(想成是wife
类);
特点:
- 构造方法设为
private
; - 调用
getInstance()
方法 返回的永远都是同一例;
饿汉式
饿汉式单例模式缺点:不管用到与否,都会实例化;
改进:
但并发编程时又会引发问题:多个线程同时访问时 发现 instance
为null
,同时实例化,这样导致有多个 wife
;
懒汉式:
用 synchronized
加锁,但带来了线程不安全的问题;
改进:保证线程安全性 DCL(double check lock ) 用volatile
修饰 instance
为什么 这里 INSTANCE 必须要用 volatile 修饰?—> DCL 单例 要不要加 volatile?
这就涉及到对象创建时启动线程的指令重排!
9. 对象创建时启动线程的指令重排现象
指令重排在对象创建过程中启动线程时也可导致问题:
如果 调用构造方法 和 建立关联语句 互换顺序,那么就会打印0;
一种半初始化的状态;
所以,有一条java编程准则就是 :不要在new
方法构造函数里面启动线程(start)
;
10. 既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
volatile
在Java中的意图是保证变量的可见性。为了实现这个功能,必须保证
1)编译器不能乱序优化;
2)指令执行在CPU上要保证读写的fence;
对于x86的体系结构,voltile
变量的访问代码会被 java 编译器生成不乱序的,带有 lock 指令前缀的机器码。而 lock 的实现还要区分 这个数据在不在 CPU 核心的专有缓存中(一般是指 L1/L2 )。如果在, MESI 才有用武之地,即 MESI 只能在一种情况下(数据在多个核心里面都被缓存)解决核心专有 Cache 之间不一致的问题。
如果有些CPU不支持 MESI 协议,那么必须用其他办法来实现等价的效果,比如总是用锁总线的方式,或者明确的 fence 指令来保证 volatile 想达到的目标。
总之,volatile 是一个高层的表达意图的“抽象”,而 MESI 是为了实现这个抽象,在某种特定情况下需要使用的一个实现细节。
参考文章:
既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
总结
volatile 三大特性:
- 可见性;
- 不保证原子性;
- 禁止指令重排;
CAS 原理:内存值V、预期值A、拟修改值,当V==A时才能令V=B;
CAS可能导致ABA问题;
- 掌握多门语言是必须的,就像建房子,该用啥就用啥,该用水泥的时候用水泥,该用木头的时候用木头;
- 学一个东西,先学门路整体架构,再丰富细节;先广度再深度;
面试准备每日系列:计算机底层之并发编程(一)原子性、atomic、CAS、ABA、可见性、有序性、指令重排、volatile、内存屏障、缓存一致性、四核八线程相关推荐
- 面试准备每日系列:计算机底层之并发编程(二)缓存行、一致性协议、伪共享、disruptor、CAS等待
文章目录 1. 缓存行 Cache line 2. 缓存一致性协议 & 伪共享 3. 为什么不加volatile? 4. 编程先可用再调优 5. disruptor & CAS等待 1 ...
- 面试准备每日系列:计算机底层之并发编程(三)JVM-垃圾回收
1. 什么是垃圾 new出来 最后不用了并且没有被回收的那块内存 就是垃圾: C.C++让用户自己回收,java.python.js等带有垃圾收集器,Golang也有(仍STW): C: malloc ...
- 计算机笔记--【并发编程①】
文章目录 并发编程 前言 1.进程与线程 1.1.概述 1.2.对比 2.并行与并发 3.同步与异步 3.1.应用之异步调用 3.2.应用之提高效率 4.Java线程 4.1.创建和运行线程 4.2. ...
- Java并发编程,无锁CAS与Unsafe类及其并发包Atomic
为什么80%的码农都做不了架构师?>>> 我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其 ...
- Java并发编程包中atomic的实现原理
转载自 Java并发编程包中atomic的实现原理 这是一篇来自粉丝的投稿,作者[林湾村龙猫]最近在阅读Java源码,这一篇是他关于并发包中atomic类的源码阅读的总结.Hollis做了一点点修 ...
- java 大厂面试指南:性能优化 + 微服务 + 并发编程 + 开源框架 + 分布式
秋招面试,我相信有人欢喜有人愁,大厂的面试题千奇百怪,不知道他会问到哪方面的知识点,我也是秋招大部队里面的一员,给大家整理出了 18 个大厂经常会问到 200 多道面试问题,涉及的知识点有,性能优化, ...
- 计算机书籍-C++并发编程实战
书名:C++并发编程实战 作者:[美] Anthony Williams 威廉姆斯 出版社:人民邮电出版社 出版时间:2015年06月 去当当网了解
- 并发编程:原子性问题,可见性问题,有序性问题。
以下是本文的目录大纲: 一.内存模型的相关概念 二.并发编程中的三个概念 三.Java内存模型 一.内存模型的相关概念 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中, ...
- java赋值语句_java并发编程之原子性问题
程序是否线程安全,取决于哪些要素呢,主要是以下三个: 原子性, 可见性, 有序性. 今天先一起来学习原子性. 原子性: 我理解一个操作不可再分,即为原子性.而在并发编程的环境中,原子性的含义就是只要该 ...
最新文章
- 华御密盾智能防信息泄密系统
- vs2015配置opencv3.3
- Windows 7/8/8.1 硬盘安装法实现 ubuntu 14.04 双系统
- 不止代码:最长上升序列
- etf基金代码大全_银行ETF最新规模首超28亿元再创历史新高,近4个月资金净流入超12亿元...
- LeetCode(合集)删除数组中的元素(26,80,283)
- 伯努利数学习笔记的说...
- MySql4.1.7 + PHP5 + Apache2.0.52(win2003下测试通过)
- 互联网原理和html基础,计算机网络基础知识习题及答案(八)
- mongodb更新操作符
- C++ explicit禁止单参构造函数隐式调用
- 科比退役原因-数据分析
- sql连接查询语句中on、where筛选的区别总结
- gerber文件怎么导贴片坐标_如何学习贴片机编程
- 用python 创建英语自定义词典
- Whois 信息与个人隐私
- 【话题:工作生活】2021年工作总结--这些人,那些事。
- Google Chrome开发者工具-移动仿真:网络带宽控制
- uni-app上传图片base64
- 尊敬的用户您好: 您访问的网站被机房安全管理系统拦截,可能是以下原因造成: 1.您
热门文章
- 笔记86 | 视频在ACC起来后会跳进度问题分析
- 浅谈Hybrid技术的设计与实现【转】
- 环信Demo 导入错误
- 让SecureCRT vi中显示多色彩
- 一位老司机开车20年后得到的26条教训
- JZOJ 1236. 邦德I
- 用贝叶斯定理解决三门问题并用Python进行模拟(Bayes‘ Rule Monty Hall Problem Simulation Python)
- 流水线冒险及解决方法
- qt project settings被禁用解决方案
- php安装redis扩展‘checking for igbinary includes... configure: error: Cannot find igbinary.h‘解决方法