Java实现原子操作的原理

叁歲伎倆 2022-09-23 10:48 329阅读 0赞

原子的定义:

原子(atomic)本意是”不能被进一步分割的最小粒子”,而原子操作描述为:“不可被中断的一个或一系列操作“。在多核处理器上实现原子操作就会变得复杂了许多。

原子操作的实现:

1.术语定义





























术语名称

英文

解释

缓存行

Cache line

缓存的最小单位

比较并交换

Compare and Swap

CAS操作需要输入两个数值,一个旧值(期望操作   前的值),一个新值,在操作期间先比较旧值有没有  发生变化,如果没有发生变化才交换成新值,发   生了变化则不交换。

CPU流水线

CPU pilelineCPU

流水线的工作方式就像工业生产上的装配流水线,在CPU中由5 5-6个不同功能的电路单元组成一条指令处理流水线,,然后将一条 X86指令分成5-6步后再由这些电路单元分别执行。这样就能实现 在一个CPU时钟周期完成一条指令,因此提高 CPU的运算速度

内存顺序冲突

Memory order violation

内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓冲行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

Center

2.处理器如何实现原子操作

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

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

  1. public class Test6 {
  2. public static void main(String[] args) {
  3. Count count=new Count();
  4. Count count2=new Count();
  5. count.start();
  6. count2.start();
  7. }
  8. }
  9. class Count extends Thread{
  10. private static int i=1;
  11. @Override
  12. public void run() {
  13. i++;
  14. System.out.println(i);
  15. super.run();
  16. }
  17. }

这里我们期望打印出2和3,但是结果有可能会出现2,2和3,3;原因可能多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中。想要保证都改写共享变量的操作都是原子的,就必须保持CPU1(线程1)读改写变量i的时候,CPU2(线程2)不能操作缓存了该共享变量内存地址的缓存。

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

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

第二个机制是通过缓存锁来保证原子性。在同一时刻,我们只需要保证对某个内存的操作是原子性即可。总线锁的开销很大,目前处理器会在某些场合使用缓存锁代替总线锁来进行优化。

有两种情况下不能使用缓存锁

第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定;

第二种情况:有些处理器不支持缓存锁定。对于Intel486和Pentium处理器,就是有缓存行也会调用总线锁定;

Java如何实现原子操作:

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

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的,自旋的CAS实现的基本思路就是循环进行CAS操作直到成功为止

  1. /*/
  2. * 计数器
  3. */
  4. public class Counter {
  5. private AtomicInteger atomic=new AtomicInteger(0);
  6. private int i=0;
  7. public static void main(String[] args) {
  8. final Counter counter=new Counter();
  9. List<Thread> list=new ArrayList<Thread>(); //创建线程集合
  10. long start=System.currentTimeMillis(); //记录下开始时间
  11. for(int j=0;j<100;j++){
  12. Thread t=new Thread(new Runnable(){
  13. @Override
  14. public void run() {
  15. for(int i=0;i<1000;i++){
  16. counter.count(); //非线程安全计数器
  17. counter.safeCount(); //线程安全
  18. }
  19. }
  20. }); //以匿名内部类的方式创建线程
  21. list.add(t);
  22. }
  23. for(Thread t:list){
  24. t.start();//启动所有线程
  25. }
  26. //等待所有线程执行完毕
  27. for(Thread t:list){
  28. try {
  29. t.join(); //得到上一个线程的锁
  30. } catch (InterruptedException e) {
  31. // TODO Auto-generated catch block
  32. e.printStackTrace();
  33. }
  34. }
  35. System.out.println(counter.i);
  36. System.out.println(counter.atomic.get());
  37. System.out.println(System.currentTimeMillis()-start);
  38. }
  39. /*/
  40. * 非线程安全计数器
  41. */
  42. private void count(){
  43. i++;
  44. }
  45. /*
  46. * 使用cas实现线程安全计数器
  47. */
  48. private void safeCount(){
  49. for(;;){
  50. int i=atomic.get();
  51. boolean bl=atomic.compareAndSet(i, ++i);
  52. if(bl){
  53. break;
  54. }
  55. }
  56. }
  57. }

上面这个类实现了一个线程安全的计数器和一个非线程安全的。在JDK1.5之后提供了一些并发包来进行原子操作,如AtomicInteger(用原子的方式更新int值),AtomicBoolean等。这些类里面还提供了自增和自减等操作的方法;

(2)CAS实现原子操作的三大问题

1.ABA问题

因为CAS需要在操作值得时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值有没有变化,但是实际上却变化了。那么ABA问题的解决思路就是使用版本号,在变量前面加上版本号,每次变量更新的时候把版本号加1,那么A-B-C就会变成1A-2B-3A。

2.循环时间长开销大

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

3.只能保证一个共享变量的原子操作

当对一个共享变量操作时,我们可以使用CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作,这个时候就可以用锁。

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

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

发表评论

表情:
评论列表 (有 0 条评论,329人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java原子操作原理剖析

    CAS的概念 ◆ 对于并发控制来说,使用锁是一种悲观的策略。它总是假设每次请求都会产生冲突,如果多个线程请求同一个资源,则使用锁宁可牺牲性能也要保证线程安全。而无...

    相关 Java实现原子操作原理

    原子的定义: 原子(atomic)本意是"不能被进一步分割的最小粒子”,而原子操作描述为:“不可被中断的一个或一系列操作“。在多核处理器上实现原子操作就会变得复杂了许多。