MST

星途 面试题库

面试题:Java内存模型中原子性与可见性的关联与区别

深入剖析Java内存模型中原子性和可见性的概念,说明它们之间的联系与区别,并且举例说明在实际多线程编程中,只保证原子性而忽略可见性可能会出现的问题及解决方案。
14.5万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

原子性

  1. 概念:原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作(除了long和double,在32位系统上,它们的读写不是原子性的,但在64位系统上是原子性的)。例如int a = 10;,这个操作要么成功将10赋值给a,要么不执行,不存在赋值一半的情况。
  2. 底层实现:Java中通过Unsafe类的一些本地方法来实现原子性操作,例如compareAndSwapInt等方法,这些方法基于CPU的CAS(Compare - And - Swap)指令,保证了在多线程环境下操作的原子性。

可见性

  1. 概念:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。在Java内存模型中,每个线程都有自己的工作内存,线程对变量的操作都在自己的工作内存中进行,而主内存是共享的。当线程修改了共享变量的值后,不会立即刷新到主内存,其他线程从主内存读取变量时,可能获取到的是旧值,从而导致可见性问题。
  2. 底层实现:通过volatile关键字来保证可见性。当一个变量被声明为volatile时,线程对该变量的写操作会立即刷新到主内存,读操作会直接从主内存读取,从而保证了其他线程能及时看到最新值。另外,synchronized关键字也能保证可见性,因为在进入 synchronized块时会从主内存读取变量,退出时会将变量刷新到主内存。

联系与区别

  1. 联系:原子性和可见性都是多线程编程中保证数据一致性的重要特性。在某些场景下,要保证数据的正确操作,既需要原子性,也需要可见性。例如在对共享变量进行复杂的读写操作时,既要保证操作的原子性,又要保证修改后其他线程能立即看到。
  2. 区别:原子性关注的是操作的完整性,是针对单个操作或指令而言;而可见性关注的是多线程之间变量值的同步,是针对多线程对共享变量的读写而言。

只保证原子性忽略可见性的问题及解决方案

  1. 问题举例
public class AtomicityWithoutVisibility {
    private static int count = 0;

    public static void increment() {
        count++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Expected: 10000, Actual: " + count);
    }
}

在上述代码中,count++操作本身不是原子性的,它包含读取、加一、写入三个步骤。但即使将count声明为AtomicInteger来保证原子性,由于没有保证可见性,不同线程对count的修改可能不会及时被其他线程看到,导致最终结果小于预期的10000。 2. 解决方案: - 使用volatile关键字:将count声明为volatile,如下:

public class AtomicityWithVisibility {
    private static volatile int count = 0;

    public static void increment() {
        count++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Expected: 10000, Actual: " + count);
    }
}
- **使用`AtomicInteger`并结合`volatile`语义**:`AtomicInteger`本身保证了原子性操作,并且其`get`和`set`方法具有`volatile`语义,也能保证可见性。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet();
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Expected: 10000, Actual: " + count.get());
    }
}