volatile和CAS及其底层原理

多个线程修改同一资源如何保证其安全性?

1. 使用synchronized锁
2. 使用lock锁
3. 使用 Atomic 原子类

多个线程争夺资源使用synchronized锁容易升级为重量级锁,如果同步代码块中只进行修改变量值这种简单的操作,那么同步代码块的执行时间远小于进程间调度所花费的时间,得不偿失。
lock锁底层由AQS+CAS机制构成,效率较synchronized有很大提升。
在这种简单的线程资源争夺场景中,使用Atomic原子类更加灵活。相对于lock的显示上锁解锁,Atomic原子类自动上锁解锁也更加安全。同时ATomic原子类内部方法都是基于 Unsafe 类实现的,Unsafe 类是个跟底层硬件CPU指令通讯的复制工具类。

Volatile保证变量的可见性

每个线程相对于主存都有一个本地内存。在执行方法前先将所需变量从主存拷贝到本地内存,然后再进行相关操作。如下场景,线程A从主存获取变量v存入本地内存,A、B的操作都是将v自增一并写回主存,但要明确一点,AB对v的操作并不是立即反应到主存中的,比如A将v值修改后又执行了其他的一系列操作,线程会在所有的操作都做完之后才会将v的值写回主存中。 如果B在A写回主存之前读取v的值,那么A、B写回的值都为v+1,显然不符合我们的预期。
Volatile的作用:在变量声明阶段用volatile关键字修饰,保证了读操作能够看到写操作的结果,这句话不好理解,说人话就是如果线程A先去写一个volatile变量,然后线程B去读这个变量,那么这个写操作的结果一定对线程B可见。(详见面试官为什么总是问happens-before规则),这样就解决了变量的可见性。这里对volatile的执行机制进行了简化,其实volatile还涉及到CPU指令相关知识。(java多线程编程之volatile和CAS)
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”—深入理解Java虚拟机
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

CAS的底层原理

Volatile关键字虽然解决了变量的可见性,但无法保证操作的原子性。举个栗子:线程AB分别到主存中读取变量v并进行v++操作,由于v++不是原子性操作,假设A已完成v++操作并写入回主存中,此时线程B也确实可以在主存中读取到v的值为v+1,不过B读取主存中v的操作是在A修改v之前完成呢?那么B执行完v++操作后并写回主存,此时v的值还是v+1。CAS的底层还是锁,只不过synchronized和lock是软件层面的锁,而CAS是硬件层面的锁。
CPU的LOCK指令分为两种锁:
总线锁: 多CPU的情况下,某个CPU发出lock指令,则可以独占总线,此时对内存的读写不会受到干扰。其他CPU没有拿到总线控制权处于等待状态。
缓存锁: 总线锁有一定的局限性,当一个CPU获取总线控制权时其他CPU只能等待,效率较低。每个CPU为了协调自身和主存的速度差距都会设置多级缓存,每次操作数据时缓存先将需要用到的数据拿到缓存中,CPU操作完成缓存再写回主存。缓存锁就是当一个CPU往主存写入数据时其他CPU查看自己的缓存中有没有这个内存地址的数据,有的话立即作废,需要使用时重新到主存中获取。

比较和交换

CAS的操作是比较和交换,参数有V(内存地址)A(旧值)B(新值)。接着上面的栗子,线程B想要修改v时需要拿着刚刚获取的旧值v去内存中获取最新的值v+1对比,如果二者不相等,说明在这期间有其他线程修改了变量v,此次操作失败。这样就能保证线程对volatile变量操作的原子性。

你可能感兴趣的:(java虚拟机,java,经验分享,面试,CAS)