搞懂i++,让面试官大吃一惊吧

看到i++大家是不是都露出了轻蔑的笑容,不就是个i++和++i的顺序问题吗?

错!!!那如果我问的是:多线程并发情况下i++是如何运行的,如何保证其原子性?

要了解到底怎么回事你起码需要掌握以下几点:

  • 并发三大特性是什么?

  • i++操作的字节码执行到底是怎样的?

  • 多线程并发情况下,什么骚操作才能保证i++的原子性呢?

你真的懂吗???

那我们接着上篇文章《还在被volatile爆锤吗?脱坑指南了解下》展示的错误使用volatile进行i++造成bug的情况来讲解。

首先处于多线程并发情况下,我们需要先知道什么是并发三大特性
  1. 原子性:原子性就是说一个操作不能被打断,要么执行完要么不执行。

  2. 可见性:可见性是指一个变量的修改对所有线程可见。即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

  3. 有序性:为了提高程序的执行性能,编辑器和处理器都有可能会对程序中的指令进行重排序。

今天这个BUG主要是由原子性造成的,所以今天这篇文章主讲原子性。

原子性举个生活当中的例子,比如
你坐过山车,不能坐一半停下来吧?
你给别人转钱肯定希望自己的钱扣了之后,别人的账户上钱到账了,也就是所有操作都能完成。而不希望出现自己的钱扣了,没有给别人转的情况。
那我们这种希望执行完一次完整操作才能进行下一次操作是希望遵循原子性的原则,emmm...但volatile不能保证i++操作的原子性。

我们知道基本数据类型的单次读、写操作是具有原子性的。同样单个volatile 变量单次的读、写操作也具有原子性。但是对于类似于 ++,--,逻辑非! 这类复合操作,这些操作整体上是不具有原子性的。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n34" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> volatile int i=0;//定义volatile变量i
 if(i==1)//单独对i读,此操作具有原子性
 i=1;//单独对i写(赋值),此操作具有原子性
 i++;//复合操作,此操作不具有原子性</pre>

因为++操作需要分三次操作完成!而不是一步执行完。要想知道为啥需要看字节码指令来找答案,知道计算机真正的操作是什么。

于是我们执行反编译命令 javap -c VolatileTest.class,我们看到increase()函数中race++是由以下字节码指令构成。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> 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
​</pre>

是不是心里在想这是什么鬼?看不懂字节码?没关系我们下面有人话。


字节码释义如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">aload_0  将this引用推送至栈顶
dup  复制栈顶值this应用,并将其压入栈顶,即此时操作数栈上有连续相同的this引用;
getfield  弹出栈顶的对象引用,获取其字段race的值并压入栈顶。第一次操作
iconst_1  将int型(1)推送至栈顶
iadd  弹出栈顶两个元素相加(race+1),并将计算结果压入栈顶。第二次操作
putfield  从栈顶弹出两个变量(累加值,this引用),将值赋值到this实例字段race上。第三次操作,赋值</pre>

如果还看不懂,没关系,看看下面的图解你就懂了。

从字节码层面很容易分析出来并发失败的原因了,假如有两条线程同时执行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++操作的原子性。直接上代码:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n56" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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());
 }
}</pre>

执行结果如下图所示:


呼~终于解决了,舒服。
别放松,新的风暴已经出现。随着我们引入了synchronized,大家是不是突然想到了魔鬼面试的灵魂拷问?synchronized和volatile的区别是什么?
所以,我们需要先将volatile到底是啥?该怎么用搞清楚,然后才能更清楚的进行对比。

本篇文章主要给大家讲解了并发三大特性中的原子性,以及i++为什么在并发场景下会出现问题,以及引入synchronized解决了i++的问题,下一篇我们来讲解:

  • volatile到底是个啥

  • 高频面试题synchronized与volatile的区别是什么

  • volatile的正确使用姿势


往期精彩:请戳《还在被volatile爆锤吗?脱坑指南了解下》

如果觉得本篇文章对您用帮助的话,麻烦动动君的小手点个赞哟~

小编将持续为程序圈的你带来技术热文,一起进步吧~


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 除了充分利用计算机处理器的能力外,一个服务端同时对多个客户端提供服务则是另一个更具体的并发应用场景。衡量一个服务性...
    胡二囧阅读 5,171评论 0 12
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 6,401评论 0 8
  • 第6章类文件结构 6.1 概述 6.2 无关性基石 6.3 Class类文件的结构 java虚拟机不和包括java...
    kennethan阅读 4,518评论 0 2
  • 本系列出于AWeiLoveAndroid的分享,在此感谢,再结合自身经验查漏补缺,完善答案。以成系统。 Java基...
    济公大将阅读 5,388评论 1 6
  • 袁立微博官宣结婚 老公是谁呢 我和这位仁兄,几乎同龄人,他看的我也基本都看过。简直收到鼓舞。 现在晚上失眠,我有时...
    名字就是个标签阅读 793评论 0 4