MST

星途 面试题库

面试题:Java 异步编程中的内存模型与可见性问题及解决方案

在 Java 异步编程环境下,由于多线程并发执行异步任务,内存模型和可见性会带来一系列问题。请详细分析这些问题产生的原因,结合 JMM(Java 内存模型)规范说明可能出现的错误场景,并提出相应的解决方案和最佳实践。
46.3万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

问题产生原因

  1. 缓存一致性问题:现代 CPU 为提高性能,每个核心都有自己的高速缓存。当多线程并发执行时,不同线程可能将共享变量缓存在不同核心的高速缓存中。如果一个线程修改了共享变量,其他线程的高速缓存中的该变量副本不会立即更新,导致内存可见性问题。
  2. 指令重排序:为了提高指令执行效率,编译器和处理器会对指令进行重排序。在单线程环境下,重排序不会影响最终执行结果,但在多线程环境中,重排序可能导致线程间的操作顺序不符合预期,从而引发内存可见性和原子性问题。

结合 JMM 规范可能出现的错误场景

  1. 可见性问题
    • 示例代码:
    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 的高速缓存。
  2. 指令重排序问题
    • 示例代码:
    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 = 1b = a 可能被重排序为 b = aa = 1。这样 t2 线程可能会打印出 a = 0,即使 b == 1,这与预期结果不符。

解决方案和最佳实践

  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.");
        }
    }
    
  2. 使用 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.");
            }
        }
    }
    
  3. 使用 java.util.concurrent.atomic 包中的原子类
    • 原子类如 AtomicIntegerAtomicBoolean 等,通过硬件级别的原子操作保证变量的原子性和可见性。
    • 示例:
    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.");
        }
    }
    
  4. 遵循 happens - before 原则
    • JMM 定义了 happens - before 原则,如程序顺序规则(一个线程内,按照代码顺序,前面的操作 happens - before 于后续的操作)、volatile 变量规则(对一个 volatile 变量的写操作 happens - before 于后续对这个 volatile 变量的读操作)等。编写代码时遵循这些原则,可以保证多线程程序的正确性。例如,在使用 volatile 变量时,就利用了 volatile 变量规则来保证可见性。