当前位置:首页 > 科技  > 软件

一个关于 i++ 和 ++i 的面试题打趴了所有人

来源: 责编: 时间:2023-09-28 10:04:33 203观看
导读大家好,我是哪吒。公司最近在招聘实习生,作为面试官之一的我,问了一道不起眼的经典面试题。一、i++和++i有啥区别?大部分的面试者会这样答:i++ 返回原来的值,++i 返回加1后的值。i++是先赋值,然后再自增;++i是先自增,后赋值。

n5m28资讯网——每日最新资讯28at.com

大家好,我是哪吒。n5m28资讯网——每日最新资讯28at.com

公司最近在招聘实习生,作为面试官之一的我,问了一道不起眼的经典面试题。n5m28资讯网——每日最新资讯28at.com

一、i++和++i有啥区别?

大部分的面试者会这样答:n5m28资讯网——每日最新资讯28at.com

  • i++ 返回原来的值,++i 返回加1后的值。
  • i++是先赋值,然后再自增;++i是先自增,后赋值。

下面这个才是主菜。n5m28资讯网——每日最新资讯28at.com

二、高并发场景下i++会遇到哪些问题?

大部分面试者心里肯定在想,这会有啥问题,不就是一个普通的操作嘛!n5m28资讯网——每日最新资讯28at.com

先从i++操作说起,一个命令可以拆分成三部分:n5m28资讯网——每日最新资讯28at.com

  • 取值
  • ++操作
  • 赋值

我去,这不是吹毛求疵,鸡蛋里挑骨头嘛!这面试不参加也罢!n5m28资讯网——每日最新资讯28at.com

但是,你想啊,如果当线程执行到取值或者++操作时,线程突然切换了,会不会有问题呢?n5m28资讯网——每日最新资讯28at.com

step1:双线程场景

public class ThreadTest1 {    int a = 1;    int b = 1;    public void add() {        System.out.println("add start");        for (int i = 0; i < 10000; i++) {            a++;            b++;        }        System.out.println("add end");    }    public void compare() {        System.out.println("compare start");        for (int i = 0; i < 10000; i++) {            boolean flag = a < b;            if (flag) {                System.out.println("a=" + a + ",b=" + b + "flag=" + flag + ",a < b = " + (a < b));            }        }        System.out.println("compare end");    }    public static void main(String[] args) {        ThreadTest1 threadTest = new ThreadTest1();        new Thread(() -> threadTest.add()).start();        new Thread(() -> threadTest.compare()).start();    }}

n5m28资讯网——每日最新资讯28at.com

哎呀我去,还真有问题,你这吹毛求疵i++三步走,逼格满满。n5m28资讯网——每日最新资讯28at.com

到底为什么会这样呢?加点日志看一下。n5m28资讯网——每日最新资讯28at.com

原来如此,两个线程交替执行了。n5m28资讯网——每日最新资讯28at.com

n5m28资讯网——每日最新资讯28at.com

step2:如何解决高并发场景下i++不安全的问题?变量上加个volatile关键字试试。

看哪吒前段时间分享的高并发系列文章,好像有一个关键字volatile,感觉挺好用,试试看。n5m28资讯网——每日最新资讯28at.com

我记得是这样的:n5m28资讯网——每日最新资讯28at.com

volatile 关键字来保证可见性和禁止指令重排。volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。n5m28资讯网——每日最新资讯28at.com


n5m28资讯网——每日最新资讯28at.com

当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。n5m28资讯网——每日最新资讯28at.com

靠谱,安排上。n5m28资讯网——每日最新资讯28at.com

n5m28资讯网——每日最新资讯28at.com

你看,好用吧,异常减少了,还得是你啊,大聪明!!!n5m28资讯网——每日最新资讯28at.com

为什么不好使呢?

1、volatile保证可见性

一个线程修改此变量后,该值会立刻刷新到主内存,其它线程每次都会从主内存中读取更新后的新值,这就保证了可见性;n5m28资讯网——每日最新资讯28at.com

简而言之,线程对volatile修饰的变量进行读写操作,都会经过主内存。n5m28资讯网——每日最新资讯28at.com

2、volatile禁止指令重排,通过内存屏障实现的

JVM编译器可以通过在程序编译生成的指令序列中插入内存屏障来禁止在内存屏障前后的指令发生重排。n5m28资讯网——每日最新资讯28at.com

volatile虽然可以保证数据的可见性和有序性,但不能保证数据的原子性n5m28资讯网——每日最新资讯28at.com

  • 读屏障插入在读指令前面,能够让CPU缓存中的数据失效,直接从主内存中读取数据;
  • 写屏障插入在写指令后面,能够让写入CPU缓存的最新数据立刻刷新到主内存;

volatile无法保证数据的原子性

step3:那怎么办?我记得可以加锁来着,都给它锁上,不就好了?

public class LockTest {    int a = 1;    int b = 1;    public void add() {        Lock lock = new ReentrantLock();        try {            lock.lock();            System.out.println("add start");            for (int i = 0; i < 10000; i++) {                a++;                b++;            }            System.out.println("add end");        } finally {            lock.unlock();        }    }    public void compare() {        Lock lock = new ReentrantLock();        try {            lock.lock();            System.out.println("compare start");            for (int i = 0; i < 10000; i++) {                boolean flag = a < b;                if (flag) {                    System.out.println("a=" + a + ",b=" + b + "flag=" + flag + ",a < b = " + (a < b));                }            }            System.out.println("compare end");        } finally {            lock.unlock();        }    }}

一顿输出猛如虎~n5m28资讯网——每日最新资讯28at.com

n5m28资讯网——每日最新资讯28at.com

我草,不玩了,我要睡了。n5m28资讯网——每日最新资讯28at.com

这又是为什么啊?n5m28资讯网——每日最新资讯28at.com

这个问题的关键是要保证变量a和b的++操作是原子性的。n5m28资讯网——每日最新资讯28at.com

那么,问题来了,lock可以解决吗?n5m28资讯网——每日最新资讯28at.com

  • Lock可以保证lock()方法和unlock()方法之间的代码是线程安全的。
  • Lock一般是通过自旋和CAS的方式进行给程序加锁,当有一个线程抢到所的资源,其他则进行等待。
  • Lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,所以unlock一般都写在finally里。
  • Lock等待锁过程中可以用interrupt来中断等待。
  • Lock可以通过trylock来知道有没有获取锁。
  • Lock可以控制锁的范围,提高多个线程进行读操作的效率。
  • ...

打住,你这和a++原子性也没关系啊。n5m28资讯网——每日最新资讯28at.com

之前出现问题,是因为add和compare交替执行造成的,lock明显是解决不了这个问题的。n5m28资讯网——每日最新资讯28at.com

lock不行的本质原因还是:synchronized是阻塞式加锁,lock是非阻塞式加锁。n5m28资讯网——每日最新资讯28at.com

step4:我记得还有一个synchronized关键字来着,加上。

为两个方法都加上synchronized关键字,确保add()方法执行时,compare()方法是不执行的。n5m28资讯网——每日最新资讯28at.com

本质原因:synchronized可以保证如果add线程获取到锁的资源,发生阻塞,compare线程会一直等待。n5m28资讯网——每日最新资讯28at.com

public class SynchronizedTest {    int a = 1;    int b = 1;    public synchronized void add() {        System.out.println("add start");        for (int i = 0; i < 10000; i++) {            a++;            b++;        }        System.out.println("add end");    }    public synchronized void compare() {        System.out.println("compare start");        for (int i = 0; i < 10000; i++) {            boolean flag = a < b;            if (flag) {                System.out.println("a=" + a + ",b=" + b + "flag=" + flag + ",a < b = " + (a < b));            }        }        System.out.println("compare end");    }}

n5m28资讯网——每日最新资讯28at.com

看到这里,高并发场景下i++会遇到哪些问题?就可以到此为止了,多角度剖析i++高并发问题。n5m28资讯网——每日最新资讯28at.com

真的没问题了吗?在所有方法上都加synchronized?效率怎么样?n5m28资讯网——每日最新资讯28at.com

n5m28资讯网——每日最新资讯28at.com

本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-11755-0.html一个关于 i++ 和 ++i 的面试题打趴了所有人

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 利用SpeechToText功能创建交互式语音助手应用程序的实现指南

下一篇: 彻底搞懂Spring的Bean加载

标签:
  • 热门焦点
Top
Baidu
map