为什么CAS加锁是线程安全的?--理解原子操作


引入

  • 在Java中实现并发用的最多的就是synchronized关键字了,自从jdk1.6对synchronized进行重大优化后,其广为人诟病的性能问题也得到了改善,与ReentrankLock相比性能方面相差无几

  • 性能的改善得益于 偏向锁、轻量级锁 的引入,它们具体的实现方式可参考《Java并发编程的艺术》和《深入理解Java虚拟机》这两本书。偏向锁、轻量级锁和重量级锁不同的地方在于不是通过信号量机制(强制阻塞)而是通过自旋CAS实现互斥访问的,避免了强制阻塞时用户态与核心态之间切换带来的开销(系统调用),这里的开销主要是保存用户态的上下文信息。

自旋CAS

  • CAS操作是一个原子操作,所谓原子操作就是指在执行期间不会被其他线程打断, 要么执行完毕,要么不执行

  • CAS操作有三个操作数V(内存地址),A(旧的预期值),B(准备设置的新值)。指令执行时先看V指向的内存中存储的值是否和A相同,如果相同才会更新为B,否则什么也不做。

  • 自旋是指当对象或同步块已经被其他线程锁定时,竞争线程空转等待占用线程执行完毕的情形。注意此时竞争线程并没有阻塞,而是原地空转,执行一个无限循环判断对象是否已解锁,所以不存在用户态到核心态的转换,因而同步效率较高(但会占用CPU时间)。

从问题出发理解原子性

  • 我一直有个疑惑, CAS的原子性和使用CAS加锁保证线程安全有什么关系? 假设有多个线程同时在对同一块内存进行CAS操作的话,那不就有可能出问题吗:两个线程T1,T2同时对同一对象执行CAS操作加锁,V存储同一块内存地址,A当然也是同样旧的预期值,那么这种情况下 T1和T2都可以进行更新 ,那么CAS操作加锁过程就是无效的,因为CAS操作成功后线程就会进入同步块,此时就会有 多个线程同时执行同步块中的代码 ······那这不就会使同步块线程不安全了吗。

  • 后来我明白了CAS原子性和线程安全的关系,在多个线程同时CAS的情况下是不会发生多个线程CAS成功的情况的,因为计算机底层实现保证了V指向内存的互斥性和立即可见性,可以理解为 CAS操作是底层保证的线程安全

  • 首先说结论,一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。

  • 处理器实现原子操作有两种做法

    • 一是 总线锁 ,在多CPU 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据

    • 二是 缓存锁 ,如果共享内存已经被缓存,那么锁总线没有意义。缓存锁核心是使用了 缓存一致性协议 ,如MESI协议

      • MSEI表示缓存行的四种状态

        • M(Modify)表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致

        • E(Exclusive)表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改

        • S(Shared)表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致

        • I(Invalid)表示缓存已经失效

      • MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作

      • CPU在读数据时,如果缓存行状态是I,则需要从内存中读取,并把缓存行状态置为S;如果不是I,则可以直接读取缓存中的值,但在此之前 必须要等待对其他CPU的监听结果 ,如果其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存后再读取

      • CPU可以将状态为M/E/S的缓存写入内存,其中 如果缓存行状态为S,则其他CPU缓存了相同数据的缓存行会无效化

  • 也就是说不会有多个线程同时访问共享变量,而且共享变量更新是对所有线程可见的,所以原子操作是线程安全的。


原文链接:https://www.cnblogs.com/codespoon/p/13462765.html