volatile型变量自增操作的隐患
用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!