引入

volatile关键字不能保证变量的原子性,什么意思呢?看以下例子:

public class TestAtomic {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}

class AtomicDemo implements Runnable {

    private volatile int number = 0;

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number++;
        System.out.println(number);
    }
}

打印结果如下:
图片说明
为什么数据会重复呢,使用了volatile关键字不是确保了数据的可见性吗?这是因为i++操作不是原子操作,i++是一个经典的读改写操作。i++做了三次指令操作,两次内存访问:

  • 第一次,从内存中读取i变量的值到CPU的寄存器
  • 第二次在寄存器中的i自增1,第三次将寄存器中的值写入内存
  • 这三次指令操作中任意两次如果同时执行的话,都会造成结果的差异性。

而对于++i,在多核机器上,CPU在读取内存时也可能同时读到同一个值,这样就会同一个值自增两次,而实际上只自增了一次,所以++i也不是原子操作。对于上述代码中第一次出现的结果,实际上是个原子操作的假象,因为线程数量少,循环次数也少,加上在执行时,虚拟机可能会对代码做部分优化,所以看起来结果是对的,而当我在代码中做一些其他的耗时的操作时,这种假象就不攻自破了。在JDK1.5之后java.util.concurrent.atomic包下提供了大量的常用的原子变量。

原子变量

jdk为我们提供的原子变量有:
图片说明
原子变量是如何解决这些问题的呢?

  • 原子变量封装的值都使用了volatile修饰保证了内存可见性图片说明
  • CAS(Compare-And-Swap)算法保证了数据的原子性。

CAS算法

CAS算法是硬件对并发操作共享数据的支持,其算法主要涉及到了三个数:

  • 内存地址 V
  • 旧的预期值 A
  • 更新值 B

主要操作流程是:CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
所以可以将上面的代码更新为如下:

public class TestAtomic {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}

class AtomicDemo implements Runnable {

    private AtomicInteger number = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 类似于i++
        number.getAndIncrement();
        System.out.println(number);
    }
}