微信扫一扫

028-83195727 , 15928970361
business@forhy.com

volatile型变量自增操作的隐患

2016-11-24

  用FindBugs跑自己的项目,报出两处An increment to a volatile field isn’t atomic。相应报错的代码如下:

volatile int num = 0;
num++;

  FindBugs针对这种类型的错误给出了相应的解释

An increment to a volatile field isn’t atomic
This code increments a volatile field. Increments of volatile fields aren’t atomic. If more than one thread is incrementing the field at the same time, increments could be lost.

  意即,对一个volatile字段进行自增操作,但这个字段不是原子类型的。如果多个线程同时对这个字段进行自增操作,可能会丢失数据。
volatile是一个轻量级的synchronized的实现,针对volatile类型变量的操作都是线程安全的。volatile类型变量每次在读取的时候,都从主存中取,而不是从各个线程的“工作内存”。而非volatile型变量每次被读取的时候都是从线程的工作内存中读取主存中变量的一份拷贝,也就意味着如果非volatile型变量被某个线程修改,其它线程读取的可能是旧值。


jvm内存模型图

  volatile类型变量每次在读取的时候,会越过线程的工作内存,直接从主存中读取,也就不会产生脏读。那为何FindBugs报这个错?

  根本原因在于++自增操作。Java的++操作对应汇编指令有三条
  1. 从主存读取变量值到cpu寄存器
  2. 寄存器里的值+1
  3. 寄存器的值写回主存

  如果N个线程同时执行到了第一步,那么最终变量会损失(N - 1)。第二步第三步只有一个线程是执行成功。
  写个demo验证这个问题

package com.alibaba.bop.tag.manager;

import org.junit.Test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * author   : lvsheng
 * date     : 2016/11/22 下午5:06
 */
public class volatileTest {

    volatile int num = 0;
    int                coreSize = Runtime.getRuntime().availableProcessors();
    ThreadPoolExecutor exec     = new ThreadPoolExecutor(coreSize * 2, coreSize * 3, 0, TimeUnit.SECONDS,
                                                                new LinkedBlockingQueue<Runnable>(500), new ThreadPoolExecutor.CallerRunsPolicy());

    @Test
    public void test() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            exec.execute(() -> num++);
        }
        System.out.println(Integer.MAX_VALUE);
        System.out.println(num);
        System.out.println("误差 : " + (Integer.MAX_VALUE - num));
    }
}

  执行结果

2147483647
2121572795
误差 :25910852

  自增操作总体上产生了1%的误差。FindBugs是个好工具,能找出自己知识体系范围外的bug,surprise!