引入
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); } }