一个关于 i 和 i 的面试题打趴了所有人
创始人
2025-07-02 19:01:00
0

大家好,我是哪吒。

公司最近在招聘实习生,作为面试官之一的我,问了一道不起眼的经典面试题。

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

大部分的面试者会这样答:

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

下面这个才是主菜。

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

大部分面试者心里肯定在想,这会有啥问题,不就是一个普通的操作嘛!

先从i++操作说起,一个命令可以拆分成三部分:

  • 取值
  • ++操作
  • 赋值

我去,这不是吹毛求疵,鸡蛋里挑骨头嘛!这面试不参加也罢!

但是,你想啊,如果当线程执行到取值或者++操作时,线程突然切换了,会不会有问题呢?

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();
    }
}

哎呀我去,还真有问题,你这吹毛求疵i++三步走,逼格满满。

到底为什么会这样呢?加点日志看一下。

原来如此,两个线程交替执行了。

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

看哪吒前段时间分享的高并发系列文章,好像有一个关键字volatile,感觉挺好用,试试看。

我记得是这样的:

volatile 关键字来保证可见性和禁止指令重排。volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。


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

靠谱,安排上。

你看,好用吧,异常减少了,还得是你啊,大聪明!!!

为什么不好使呢?

1、volatile保证可见性

一个线程修改此变量后,该值会立刻刷新到主内存,其它线程每次都会从主内存中读取更新后的新值,这就保证了可见性;

简而言之,线程对volatile修饰的变量进行读写操作,都会经过主内存。

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

JVM编译器可以通过在程序编译生成的指令序列中插入内存屏障来禁止在内存屏障前后的指令发生重排。

volatile虽然可以保证数据的可见性和有序性,但不能保证数据的原子性

  • 读屏障插入在读指令前面,能够让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();
        }
    }
}

一顿输出猛如虎~

我草,不玩了,我要睡了。

这又是为什么啊?

这个问题的关键是要保证变量a和b的++操作是原子性的。

那么,问题来了,lock可以解决吗?

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

打住,你这和a++原子性也没关系啊。

之前出现问题,是因为add和compare交替执行造成的,lock明显是解决不了这个问题的。

lock不行的本质原因还是:synchronized是阻塞式加锁,lock是非阻塞式加锁。

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

为两个方法都加上synchronized关键字,确保add()方法执行时,compare()方法是不执行的。

本质原因:synchronized可以保证如果add线程获取到锁的资源,发生阻塞,compare线程会一直等待。

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");
    }
}

看到这里,高并发场景下i++会遇到哪些问题?就可以到此为止了,多角度剖析i++高并发问题。

真的没问题了吗?在所有方法上都加synchronized?效率怎么样?

相关内容

热门资讯

PHP新手之PHP入门 PHP是一种易于学习和使用的服务器端脚本语言。只需要很少的编程知识你就能使用PHP建立一个真正交互的...
网络中立的未来 网络中立性是什... 《牛津词典》中对“网络中立”的解释是“电信运营商应秉持的一种原则,即不考虑来源地提供所有内容和应用的...
各种千兆交换机的数据接口类型详... 千兆交换机有很多值得学习的地方,这里我们主要介绍各种千兆交换机的数据接口类型,作为局域网的主要连接设...
粉嫩如何诠释霸道 东芝M805... “霸道粉”是个什么玩意东芝M805拿过来的时候,笔者扑哧笑了,不是笑这款笔记本,而是笑这款产品的颜色...
什么是大数据安全 什么是大数据... 在《为什么需要大数据安全分析》一文中,我们已经阐述了一个重要观点,即:安全要素信息呈现出大数据的特征...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
全面诠释网络负载均衡 负载均衡的出现大大缓解了服务器的压力,更是有效的利用了资源,提高了效率。那么我们现在来说一下网络负载...
如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
30分钟搞定iOS自定义相机 最近公司的项目中用到了相机,由于不用系统的相机,UI给的相机切图,必须自定义才可以。就花时间简单研究...
Intel将Moblin社区控... 本周二,非营利机构Linux基金会宣布,他们将担负起Moblin社区的管理工作,而这之前,Mobli...