大家好,我是哪吒。
公司最近在招聘实习生,作为面试官之一的我,问了一道不起眼的经典面试题。
大部分的面试者会这样答:
下面这个才是主菜。
大部分面试者心里肯定在想,这会有啥问题,不就是一个普通的操作嘛!
先从i++操作说起,一个命令可以拆分成三部分:
我去,这不是吹毛求疵,鸡蛋里挑骨头嘛!这面试不参加也罢!
但是,你想啊,如果当线程执行到取值或者++操作时,线程突然切换了,会不会有问题呢?
package com.guor.thread;
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++三步走,逼格满满。
到底为什么会这样呢?加点日志看一下。
原来如此,两个线程交替执行了。
看哪吒前段时间分享的高并发系列文章,好像有一个关键字volatile,感觉挺好用,试试看。
我记得是这样的:
volatile 关键字来保证可见性和禁止指令重排。volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。
靠谱,安排上。
你看,好用吧,异常减少了,还得是你啊,大聪明!!!
1、volatile保证可见性
一个线程修改此变量后,该值会立刻刷新到主内存,其它线程每次都会从主内存中读取更新后的新值,这就保证了可见性;
简而言之,线程对volatile修饰的变量进行读写操作,都会经过主内存。
2、volatile禁止指令重排,通过内存屏障实现的。
JVM编译器可以通过在程序编译生成的指令序列中插入内存屏障来禁止在内存屏障前后的指令发生重排。
volatile虽然可以保证数据的可见性和有序性,但不能保证数据的原子性。
package com.guor.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
volatile int a = 1;
volatile 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();
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
new Thread(() -> lockTest.add()).start();
new Thread(() -> lockTest.compare()).start();
}
}
一顿输出猛如虎~
我草,不玩了,我要睡了。
这又是为什么啊?
这个问题的关键是要保证变量a和b的++操作是原子性的。
那么,问题来了,lock可以解决吗?
打住,你这和a++原子性也没关系啊。
之前出现问题,是因为add和compare交替执行造成的,lock明显是解决不了这个问题的。
lock不行的本质原因还是:synchronized是阻塞式加锁,lock是非阻塞式加锁。
为两个方法都加上synchronized关键字,确保add()方法执行时,compare()方法是不执行的。
本质原因:synchronized可以保证如果add线程获取到锁的资源,发生阻塞,compare线程会一直等待
。
package com.guor.thread;
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");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new Thread(() -> synchronizedTest.add()).start();
new Thread(() -> synchronizedTest.compare()).start();
}
}
看到这里,高并发场景下i++会遇到哪些问题?
就可以到此为止了,多角度剖析i++高并发问题。
真的没问题了吗?在所有方法上都加synchronized?效率怎么样?
我们下期再聊~
哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师。