AtomicInteger类和int以及i++的线程安全问题
问题:i++是线程安全的吗?
这个问题可以从两个方面回答
- 若是局部变量,那么i++是线程安全;
- 若是全局变量,那么i++非线程安全。
原因:
- 若是局部变量,那其他线程也访问不到,所以根本不存在是否安全这个问题。
- 若是全局变量,任意线程都可以访问,而i++这个操作是非原子性的,这个会编译成 i = i +1;这里做了多个操作,包括 读取,修改,写入 。并发情况下会出现访问冲突。
举个例子:
比如有200个线程同时执行i++操作,会存在比如同时两个线程当前获得i的值是10,然后同时执行++操作,都执行完后,最后得到的是11,所以线程不安全。
int自增实验
测试实例:
/**
* 全局共享变量
*/
public static int count = 0;
/**
* 测试
* count ++
* 期望结果: 2*10000=20000
* 实际结果:总小于 20000
*/
@Test
public void testInt() throws Exception {
//创建线程池
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(200);
for (int i = 0; i < 10000; i++) {
scheduler.execute(() -> {
for (int j = 0; j < 2; j++) {
System.out.println("线程:" + Thread.currentThread().getName() + " count=" + count++);
}
});
}
scheduler.shutdown();
Thread.sleep(1000);
System.out.println("最终结果是 :" + count);
}
测试结果
由此可见,这种自增方式是线程不安全的,怎么解决呢?
第一个想到的就是加锁synchronized或者Lock,这里暂不介绍加锁的方式。
使用了锁去做计数的话系统的性能将会大大下降,有没有更优雅的方式呢?
这里还可以使用 AtomicInteger
AtomicInteger 自增
测试实例
/**
* 全局共享变量
*/
private static AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 测试
* atomicInteger ++
* 期望结果: 2*10000=20000
* 实际结果: 20000
*/
@Test
public void testAtomicInteger() throws Exception {
//创建线程池
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(200);
for (int i = 0; i < 10000; i++) {
scheduler.execute(() -> {
for (int j = 0; j < 2; j++) {
//自增并返回当前值
int andIncrement = atomicInteger.incrementAndGet();
System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
}
});
}
scheduler.shutdown();
Thread.sleep(1000);
System.out.println("最终结果是 :" + atomicInteger.get());
}
测试结果
原因分析
源码
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
AtomicInteger类中变量valueOffset,用来记录AtomicInteger类中value的内存位置 。
当需要变量的值改变的时候,先通过get()得到valueOffset位置的值,也即当前value的值.给该值进行增加,并赋给next
巧妙之处!
compareAndSet()比较之前取到的value的值当前有没有改变,若没有改变说明没有被其它线程修改,就将next的值赋给value,倘若和之前的值相比的话发生变化的话,则重新一次循环,直到存取成功,通过这样的方式能够保证该变量是线程安全的
优点总结:
- 线程安全,因为AtomicInteger由硬件提供原子操作指令实现的
- 在非激烈竞争的情况下,开销更小,速度更快。
- 使用非阻塞算法来实现并发控制的,可以避免多线程的优先级倒置和死锁情况的发生,提升在高并发处理下的性能。
缺点总结
- AtomicInteger,如果在线程较多的情况下,效率会变的很低,因为没有加锁,其他线程会频繁打断存取的过程,导致较低
- 使用一定要注意越界问题
AtomicInteger 越界问题
现象:当AtomicInteger增加到了2147483647(Integer.MAX_VALUE)再加一,值会变成负数-2147483648(Integer.MIN_VALUE)。当资源数目不断累积超过最大值变成负数的时候,最后产生的值中会带有一个“-”
测试实例
private static AtomicInteger atomicInteger = new AtomicInteger(Integer.MAX_VALUE);
public static void main(String[] args) {
System.out.println("初始值 : " + atomicInteger);
for (int i = 0; i < 10; i++) {
System.out.println("当前值 : "+ atomicInteger.incrementAndGet());
}
}
测试结果
使用建议
在高并发以及微服务项目中,技术操作建议使用 Redis 计数器。缓存操作速度快,单线程无竞争。
附
- AtomicInteger常用接口
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
public final int incrementAndGet() //获取自增后的值
public final int decrementAndGet()//获取自减后的值
2.Java的原始数据类型(primitive datatypes),
如short、int、double、long、boolean这些是非线程安全的;
3.Java自带的线程安全的基本类型包括:
AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等