看到i++大家是不是都露出了轻蔑的笑容,不就是个i++和++i的顺序问题吗?
错!!!那如果我问的是:多线程并发情况下i++是如何运行的,如何保证其原子性?
要了解到底怎么回事你起码需要掌握以下几点:
- 并发三大特性是什么?
- i++操作的字节码执行到底是怎样的?
- 多线程并发情况下,什么骚操作才能保证i++的原子性呢?
你真的懂吗???
那我们接着上篇文章《还在被volatile爆锤吗?脱坑指南了解下》展示的错误使用volatile进行i++造成bug的情况来讲解。
首先处于多线程并发情况下,我们需要先知道什么是并发三大特性
-
原子性:原子性就是说一个操作不能被打断,要么执行完要么不执行。
-
可见性:可见性是指一个变量的修改对所有线程可见。即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
-
有序性:为了提高程序的执行性能,编辑器和处理器都有可能会对程序中的指令进行重排序。
今天这个BUG主要是由原子性造成的,所以今天这篇文章主讲原子性。
原子性举个生活当中的例子,比如
你坐过山车,不能坐一半停下来吧?
你给别人转钱肯定希望自己的钱扣了之后,别人的账户上钱到账了,也就是所有操作都能完成。而不希望出现自己的钱扣了,没有给别人转的情况。
那我们这种希望执行完一次完整操作才能进行下一次操作是希望遵循原子性的原则,emmm…但volatile不能保证i++操作的原子性。
我们知道基本数据类型的单次读、写操作是具有原子性的。同样单个volatile 变量单次的读、写操作也具有原子性。但是对于类似于 ++,–,逻辑非! 这类复合操作,这些操作整体上是不具有原子性的。
volatile int i=0;//定义volatile变量i
if(i==1)//单独对i读,此操作具有原子性
i=1;//单独对i写(赋值),此操作具有原子性
i++;//复合操作,此操作不具有原子性
因为++操作需要分三次操作完成!而不是一步执行完。要想知道为啥需要看字节码指令来找答案,知道计算机真正的操作是什么。
于是我们执行反编译命令 javap -c VolatileTest.class,我们看到increase()函数中race++是由以下字节码指令构成。
public void increase();
Code:
0: aload_0
1: dup
2: getfield #2 // Field race:I
5: iconst_1
6: iadd
7: putfield #2 // Field race:I
10: return
是不是心里在想这是什么鬼?看不懂字节码?没关系我们下面有人话。
字节码释义如下:
aload_0 将this引用推送至栈顶
dup 复制栈顶值this应用,并将其压入栈顶,即此时操作数栈上有连续相同的this引用;
getfield 弹出栈顶的对象引用,获取其字段race的值并压入栈顶。第一次操作
iconst_1 将int型(1)推送至栈顶
iadd 弹出栈顶两个元素相加(race+1),并将计算结果压入栈顶。第二次操作
putfield 从栈顶弹出两个变量(累加值,this引用),将值赋值到this实例字段race上。第三次操作,赋值
如果还看不懂,没关系,看看下面的图解你就懂了。
从字节码层面很容易分析出来并发失败的原因了,假如有两条线程同时执行race++,
(1)线程A,线程B同时执行getfield指令把race的值压入各自的操作栈顶时。volatile关键字可以保证来race的值在此时是正确(最新的值)的。
(2)线程A,线程B同时执行iconst_1将int型(1)推送至栈顶
(3)线程A依次执行完了后续操作iadd和putfield,此时主内存中race的值已被增大1。线程A执行完毕后,线程B操作栈顶的race值就变成了过期的数据。
(4)这时线程B执行iadd、putfield后就会把较小的值同步会主内存了。
所以,大家就会发现我本来希望线程B通过++操作后对我的数据进行增加的操作失效了,这就是为什么我们之前执行的结果值始终偏小的原因。
在这种场景中,就不建议使用volatile了,我们需要通过加锁来保证原子性,也就是使用synchronized。
如何解决i++问题
synchronized具有原子性,所以我们可以通过synchronized保证 race++操作的原子性。直接上代码:
public class SynchronizedTest {
public int race = 0;
//使用synchronized保证++操作原子性
public synchronized void increase() {
race++;
}
public int getRace(){
return race;
}
public static void main(String[] args) {
//创建5个线程,同时对同一个volatileTest实例对象执行累加操作
SynchronizedTest synchronizedTest=new SynchronizedTest();
int threadCount = 10;
Thread[] threads = new Thread[threadCount];//5个线程
for (int i = 0; i < threadCount; i++) {
//每个线程都执行1000次++操作
threads[i] = new Thread(()->{
for (int j = 0; j < 10000; j++) {
synchronizedTest.increase();
}
System.out.println(synchronizedTest.getRace());
});
threads[i].start();
}
//等待所有累加线程都结束
for (int i = 0; i < threadCount; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//所有子线程结束后,race是:5*10000=50000。
System.out.println("累加结果:"+synchronizedTest.getRace());
}
}
执行结果如下图所示:
呼~终于解决了,舒服。
别放松,新的风暴已经出现。随着我们引入了synchronized,大家是不是突然想到了魔鬼面试的灵魂拷问?synchronized和volatile的区别是什么?
所以,我们需要先将volatile到底是啥?该怎么用搞清楚,然后才能更清楚的进行对比。
本篇文章主要给大家讲解了并发三大特性中的原子性,以及i++为什么在并发场景下会出现问题,以及引入synchronized解决了i++的问题,下一篇我们来讲解:
- volatile到底是个啥
- 高频面试题synchronized与volatile的区别是什么
- volatile的正确使用姿势
往期精彩,猛戳《还在被volatile爆锤吗?脱坑指南了解下》