面试题答案
一键面试问题产生原因
- 缓存一致性问题:现代 CPU 为提高性能,每个核心都有自己的高速缓存。当多线程并发执行时,不同线程可能将共享变量缓存在不同核心的高速缓存中。如果一个线程修改了共享变量,其他线程的高速缓存中的该变量副本不会立即更新,导致内存可见性问题。
- 指令重排序:为了提高指令执行效率,编译器和处理器会对指令进行重排序。在单线程环境下,重排序不会影响最终执行结果,但在多线程环境中,重排序可能导致线程间的操作顺序不符合预期,从而引发内存可见性和原子性问题。
结合 JMM 规范可能出现的错误场景
- 可见性问题:
- 示例代码:
public class VisibilityProblem { private static boolean flag = false; public static void main(String[] args) { new Thread(() -> { while (!flag) { // 线程 1 等待 flag 变为 true } System.out.println("Thread 1 stopped."); }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("Main thread set flag to true."); } }
- 分析:在这个例子中,主线程修改了
flag
变量,但由于可见性问题,线程 1 可能永远不会看到flag
的变化,从而陷入死循环。这是因为线程 1 可能将flag
缓存在自己的高速缓存中,主线程对flag
的修改没有及时同步到线程 1 的高速缓存。
- 指令重排序问题:
- 示例代码:
public class ReorderingProblem { private static int a = 0; private static int b = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000000; i++) { a = 0; b = 0; Thread t1 = new Thread(() -> { a = 1; b = a; }); Thread t2 = new Thread(() -> { if (b == 1) { System.out.println("a = " + a); } }); t1.start(); t2.start(); t1.join(); t2.join(); } } }
- 分析:按照正常逻辑,如果
b == 1
,那么a
肯定为 1。但由于指令重排序,t1
线程中a = 1
和b = a
可能被重排序为b = a
,a = 1
。这样t2
线程可能会打印出a = 0
,即使b == 1
,这与预期结果不符。
解决方案和最佳实践
- 使用 volatile 关键字:
volatile
关键字可以保证变量的可见性,当一个变量被声明为volatile
时,对它的写操作会立即刷新到主内存,读操作会从主内存读取最新值。- 修改可见性问题示例代码:
public class VisibilitySolution { private static volatile boolean flag = false; public static void main(String[] args) { new Thread(() -> { while (!flag) { // 线程 1 等待 flag 变为 true } System.out.println("Thread 1 stopped."); }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("Main thread set flag to true."); } }
- 使用 synchronized 关键字:
synchronized
关键字不仅可以保证线程安全,还能保证可见性。进入synchronized
块时,会从主内存读取共享变量,退出时会将共享变量刷新到主内存。- 示例:
public class SynchronizedSolution { private static boolean flag = false; public static void main(String[] args) { new Thread(() -> { while (true) { synchronized (SynchronizedSolution.class) { if (flag) { System.out.println("Thread 1 stopped."); break; } } } }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (SynchronizedSolution.class) { flag = true; System.out.println("Main thread set flag to true."); } } }
- 使用 java.util.concurrent.atomic 包中的原子类:
- 原子类如
AtomicInteger
、AtomicBoolean
等,通过硬件级别的原子操作保证变量的原子性和可见性。 - 示例:
import java.util.concurrent.atomic.AtomicBoolean; public class AtomicSolution { private static AtomicBoolean flag = new AtomicBoolean(false); public static void main(String[] args) { new Thread(() -> { while (!flag.get()) { // 线程 1 等待 flag 变为 true } System.out.println("Thread 1 stopped."); }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag.set(true); System.out.println("Main thread set flag to true."); } }
- 原子类如
- 遵循 happens - before 原则:
- JMM 定义了 happens - before 原则,如程序顺序规则(一个线程内,按照代码顺序,前面的操作 happens - before 于后续的操作)、volatile 变量规则(对一个 volatile 变量的写操作 happens - before 于后续对这个 volatile 变量的读操作)等。编写代码时遵循这些原则,可以保证多线程程序的正确性。例如,在使用
volatile
变量时,就利用了 volatile 变量规则来保证可见性。
- JMM 定义了 happens - before 原则,如程序顺序规则(一个线程内,按照代码顺序,前面的操作 happens - before 于后续的操作)、volatile 变量规则(对一个 volatile 变量的写操作 happens - before 于后续对这个 volatile 变量的读操作)等。编写代码时遵循这些原则,可以保证多线程程序的正确性。例如,在使用