原子性

Posted figsprite

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原子性相关的知识,希望对你有一定的参考价值。

    我们先聊聊一个经典面试题:i=i++; 我们知道在虚拟机底层它实际上做了三步:

    int temp =i; i = i + 1; i = temp;

    i++实际上的操作分为三个部分:读、改、写

    我们看看下面的例子:

  1. public class TestAtomicDemo {  
  2.     public static void main(String[] args) {  
  3.         AtomicDemo ad = new AtomicDemo();  
  4.             
  5.         for(int i = 0;i<10;i++) {  
  6.             new Thread(ad).start();  
  7.         }  
  8.     }  
  9. }  
  10.     
  11. class AtomicDemo implements Runnable{  
  12.     private int serialNumber = 0;  
  13.         
  14.     @Override  
  15.     public void run() {  
  16.         // TODO Auto-generated method stub  
  17.         try {  
  18.             Thread.sleep(200);  
  19.         }catch(InterruptedException e) {  
  20.                 
  21.         }  
  22.             
  23.         System.out.println(Thread.currentThread().getName()+":"+getSerialNumber() );  
  24.     }  
  25.     
  26.     public int getSerialNumber() {  
  27.         return serialNumber++;  
  28.     }  
  29.     
  30.     public void setSerialNumber(int serialNumber) {  
  31.         this.serialNumber = serialNumber;  
  32.     }  
  33.         
  34.         
  35. }  

技术图片

 

    这里很明显,两个数据相同了,当然这不是每次都会发生的。为什么会发生这种情况?

技术图片

 

    为了方便我们将变量名写成Num,就只画四个线程,根据我们之前说的,num先被线程1读取,然后执行改、写操作,在线程1还没来得及将值写回主存时,线程2读取了主存中的num,此时num依然等于0,这就发生了上面很尴尬的场景了。

    我们可以用上一讲谈到的volatile解决吗?当然不行,volatile解决的是可见性,然而我们这里有两步操作,即使是直接在主存操作,依旧可能出现上述情况,即线程1刚刚把num加完正打算写之前,线程2读取了当前num的值,最后依然出现两个1,因为i++存在读改写三步操作,本来这些操作不可分割,但是现在分割执行,破坏了原子性,我们现在需要用到原子变量。

    JDK1.5之后,java.util.concurrent.atomic为我们提供了大量的原子变量,这些变量有以下几个特点:

  1. 含有volatile的特性,原子变量封装的值都是volatile,保证内存的可见性,随便开个源码都可看到:

    技术图片

     

  2. CAS(Compare-And-Swap)算法保证数据原子可见性

    CAS算法是硬件对并发操作共享数据的支持

    CAS包含了三个操作数:

    内存值V 预估值A 更新值B

    当且仅当V==A时,V=B,否则将不会做任何操作

    技术图片

 

    那么CAS如何解决原子性问题呢?

    当线程1改完值,将主存中的num值修改为1,此时线程2需要进行判断,发现原值与现在值不同,所以什么都不做。这样只会使一个线程修改num,更重要的是CAS算法比普通的同步锁效率高很多,当CAS判定失败,它会放弃对CPU的占用,然后再进行判断。缺点就是我们要手工写一些代码,比如重新判断。

 

    扯了这么多,我们回到原先的题目,使用AtomicInteger来处理原问题的原子性,

技术图片

 

  1. import java.util.concurrent.atomic.AtomicInteger;  
  2.     
  3. public class TestAtomicDemo {  
  4.     public static void main(String[] args) {  
  5.         AtomicDemo ad = new AtomicDemo();  
  6.             
  7.         for(int i = 0;i<10;i++) {  
  8.             new Thread(ad).start();  
  9.         }  
  10.     }  
  11. }  
  12.     
  13. class AtomicDemo implements Runnable{  
  14.     private AtomicInteger serialNumber = new AtomicInteger(0);  
  15.         
  16.     @Override  
  17.     public void run() {  
  18.         // TODO Auto-generated method stub  
  19.         try {  
  20.             Thread.sleep(200);  
  21.         }catch(InterruptedException e) {  
  22.                 
  23.         }  
  24.             
  25.         System.out.println(Thread.currentThread().getName()+":"+getSerialNumber() );  
  26.     }  
  27.     
  28.     public int getSerialNumber() {  
  29.         return serialNumber.getAndIncrement();  
  30.     }     
  31. }  

以上是关于原子性的主要内容,如果未能解决你的问题,请参考以下文章

Redis如何保证原子性

原子性、可见性、有序性

JAVA的原子性和可见性,线程同步的理解

并发编程的原子性 != 事务ACID的原子性

原子性和原子性操作

并发研究之可见性有序性原子性