i++的线程安全性问题分析
背景
今天分享一道常见的面试题:i++是线程安全的吗?
既然这么问了,答案肯定是不安全啊,至于为啥不安全,咱们来说道说道
分析
前提
谈到线程安全问题,那什么情况下会出现线程安全的问题呢,就是当多个线程操作同一个共享变量的时候,就会出现线程安全问题;那共享变量又是指哪些呢,就是存储在堆中即主内存的变量信息,包括全局变量、对象实例、静态变量等。
而在方法内部的声明的临时变量是不会存在线程安全问题的,因为这些变量是存储在线程的工作内存中(即私有内存),线程与线程间是无法共享的。
**所以,我们讨论的线程安全问题一定是针对于全局变量而言,那就要区别面试题中i的范围,如果i是全局变量,则会出现安全问题;如果i是局部变量,则是线程安全的。**
java中对共享变量的操作原理
稍微做一下解释:共享变量存储在主内存中,当某个线程需要对共享变量进行操作时,需要将共享变量拷贝一份到自己的工作内存中(线程私有),操作完成之后,就会将最新的结果刷新到主存中。
这里的线程安全问题就在于,当一个线程将主存中的数据读取到自己的工作内存之后,没来操作完成,那另一个线程又将主存数据读取并操作,很显然,前者对于主存变量的操作就会被覆盖,从而引发线程安全问题。
解决方案
方案一(常见错误方案)
使用volatile字段对共享变量进行修饰。
volatile字段的作用是让改变量对其他所有线程可见,但是并不能保证操作的原子性。仍然会出现多个线程同时读取主内存变量的情况。
附一张volatile的原理图:
方案二
加同步锁,比如使用synchronized关键字修饰,保证只有一个线程可以对主存变量进行操作。
public class demo {
private int value;
public synchronized void increase() {
value++;
}
}
方案三
使用Atomic*类修饰来保证原子性
public class demo {
private AtomicInteger value;
public void increase() {
value.incrementAndGet();
}
}
附:i++的字节码分析
以上说的都是从程序原理上说明的,咱们还可直接看一下底层的字节码是如何实现i++操作的。
java程序如下:
public class demo {
private int value;
public void increase() {
value++;
}
}
现将java代码进行编译,然后使用字节码查看命令,javap -v demo.class,如下:
{
public com.imooc.miaoshaproject.demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public void increase();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field value:I
5: iconst_1
6: iadd
7: putfield #2 // Field value:I
10: return
LineNumberTable:
line 11: 0
line 12: 10
}
咱们来主要看一下increase这个方法,很明显,在进行i++进行操作的时候,是现将i值取出,然后进行iadd操作,最后再调用putfield方法进行赋值,这里的操作并不能保证原子性,如果在多线程中,很可能会出现以下情况
Thread1 | Thread2 |
---|---|
r1=i | r3=i |
r2=r1+1 | r4=r3+1 |
i=r2 | i=r4 |
总结
1、i++作用域在局部方法中是不会出现线程安全问题的,只有在全局变量中才会出现线程安全问题
2、volatile只能保证变量对其他线程的可见性,并不能保证原子性操作
3、可以对i++操作使用同步锁,或者使用Atomic*包修饰共享变量,来保证原子性操作
上一篇: Druid连接池的意义以及使用