大家好,我是哪吒。
公司最近在招聘实习生,作为面试官之一的我,问了一道不起眼的经典面试题。
一、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?效率怎么样?