ICode9

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

java并发编程系列之一:原子性

2020-04-03 09:06:22  阅读:184  来源: 互联网

标签:缓存 java CAS 编程 原子 并发 指令 处理器 操作


1. 线程安全的定义:

当多个线程访问某个类时,该类始终都能表现出正确的行为,那么这个类就是线程安全的:当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为。

我们可以看出线程安全的代码必须有一个特征:代码本身封装了所有必要的正确性保障手段,如互斥同步等,调用者无需关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。

并发编程的三个概念:原子性,可见性以及有序性。

2. 原子操作:

2.1 原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。我们看看在处理器以及java中是怎么实现它的。

2.1.1 处理器如何实现原子操作:

(1)使用总线锁保证原子性

         如果多个处理器同时对共享变量进行读改写操作,(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。

         处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

(2)使用缓存锁保证原子性

         在同一时刻,我们只需保证对某个内存地址,的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。    

       但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。针对以上两个机制,我们通过Intel处理器提供了很多Lock前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其他一些操作数和逻辑指令(如ADD、OR)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。   

总的来说,对于单处理器单核系统来说,只要保证操作指令序列不被打断即可实现原子操作(当然,对于内存的读写操作,需要地址对齐,否则就不是一次的内存读写了,当然也就不是原子操作)。对于简单的原子操作,cpu实现上会提供单条指令,比如INC和XCHG。对于复杂的原子操作,需要包含多条指令。执行过程中,出现上下文切换行为,比如任务切换,中断处理等。这里的行为会影响原子操作的原子性。因此需要自旋锁spinlock来保证操作指令序列不会在执行的中途受干扰。

但是如果对于多处理器或者多核的系统,原子操作的实现除了需要spinlock来保证外,还需要保证不会受到同处理器上其他核,或者其他处理器的影响。当其他核上执行的指令访问的内存空间,与当前原子操作需要访问的内存空间存在冲突时,就会破坏原子操作的正确性。

在x86架构中,提供了指令前缀LOCK。LOCK保证了指令不会受其他处理器或cpu核的影响。在PentiumPro之前,LOCK的实现,是通过锁住bus(总线),从而阻止其他cpu核的内存访问。可想而知,这种实现是非常低效的。从PentiumPro开始,LOCK只会阻塞其他cpu核对相关内存的缓存块的访问。

现在,大多数的x86处理器都支持了CAS的硬件实现,保证了多处理器多核系统下的原子操作的正确性。CAS的实现同样无需锁住总线,只会阻塞其他cpu核对相关内存的缓存块的访问。同样的,在MIPS和ARM架构下,还支持了LL/SC的实现。LL/SC不会出现CAS中的ABA问题,而且基于LL/SC可以实现CAS FAA等原子操作。

2.1.2 java中如何实现原子性:

(1)使用循环CAS实现原子操作

        JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是思路就是循环进行CAS操作直到成功为止。但是CAS操作有三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。

       针对ABA问题,解决思路是增加了版本号控制,从java5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

       自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。


  (2) 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。划重点:除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

 

 

参考:https://zhuanlan.zhihu.com/p/33445834

 

标签:缓存,java,CAS,编程,原子,并发,指令,处理器,操作
来源: https://blog.csdn.net/yuxiaolang2008/article/details/105236027

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

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

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

ICode9版权所有