核心结论

对于32位及以下的基本类型(byte、short、int、float、char、boolean),单次赋值操作是原子性的。但对于64位类型(long、double),在32位JVM上可能不是原子性的。

JVM规范定义

Java内存模型(JMM)的保证

根据《Java语言规范》第17.7节:

对于除long和double之外的所有基本类型,单个变量的读写操作是原子的。

// 原子性保证
int a = 1;        // ✅ 原子操作(32位)
short b = 2;      // ✅ 原子操作(16位)
byte c = 3;       // ✅ 原子操作(8位)
boolean d = true; // ✅ 原子操作(8位)
char e = 'A';     // ✅ 原子操作(16位)
float f = 1.0f;   // ✅ 原子操作(32位)

// 可能非原子
long g = 1L;      // ⚠️ 在32位JVM上可能非原子(64位)
double h = 1.0;   // ⚠️ 在32位JVM上可能非原子(64位)

原理分析

1. 32位类型的原子性

CPU指令层面

int a = 1;

对应的汇编指令(x86)

mov DWORD PTR [rbp-4], 0x1  ; 单条MOV指令

特点

  • 单条机器指令完成
  • CPU保证单条指令的不可分割性
  • 即使在多核环境下也是原子的

内存对齐的影响

public class MemoryAlignment {
    int a;  // 假设地址:0x1000(4字节对齐)
    
    // 如果地址对齐,单条指令完成写入
    // 如果地址未对齐(跨越缓存行边界),可能需要多次内存访问
}

现代JVM的保证

  • 对象字段自动对齐到自然边界
  • int类型对齐到4字节边界
  • 保证单次内存访问完成

2. 64位类型的非原子性

问题:32位JVM上的long/double

// 在32位JVM上
long value = 0x0123456789ABCDEFL;

// 可能分解为两次32位操作:
高32位0x01234567
低32位0x89ABCDEF

汇编指令(32位x86)

; 写入long需要两条指令
mov DWORD PTR [ebp-8], 0x89ABCDEF   ; 低32位
mov DWORD PTR [ebp-4], 0x01234567   ; 高32位

; 问题:两条指令之间可能被中断

并发问题示例

public class LongAtomicityProblem {
    private long value = 0L;  // 初始值:0x0000000000000000
    
    public static void main(String[] args) throws InterruptedException {
        LongAtomicityProblem obj = new LongAtomicityProblem();
        
        // 线程1:写入0x1111111111111111
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                obj.value = 0x1111111111111111L;
            }
        });
        
        // 线程2:写入0x2222222222222222
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                obj.value = 0x2222222222222222L;
            }
        });
        
        // 线程3:读取
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                long v = obj.value;
                // 在32位JVM上,可能读到混合值:
                // 0x1111111122222222(高位来自t1,低位来自t2)
                // 0x2222222211111111(高位来自t2,低位来自t1)
                if (v != 0 && v != 0x1111111111111111L && v != 0x2222222222222222L) {
                    System.out.println("检测到非原子操作:" + Long.toHexString(v));
                }
            }
        });
        
        t1.start();
        t2.start();
        t3.start();
        
        t1.join();
        t2.join();
        t3.join();
    }
}

可能输出(32位JVM)

检测到非原子操作:1111111122222222
检测到非原子操作:2222222211111111

3. volatile的作用

public class VolatileLong {
    private volatile long value = 0L;  // 使用volatile
    
    // volatile保证:
    // 1. 即使在32位JVM上,long/double的读写也是原子的
    // 2. 内存可见性
    // 3. 禁止指令重排序
}

实际测试

测试1:int赋值的原子性

public class IntAtomicityTest {
    private int value = 0;
    
    public static void main(String[] args) throws InterruptedException {
        IntAtomicityTest obj = new IntAtomicityTest();
        
        // 100个线程并发写入不同值
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            final int finalI = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    obj.value = finalI;  // 每次写入都是原子的
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("最终值:" + obj.value);
        // 输出:0-99之间的某个完整值(不会是混合值)
        // 证明:单次写入是原子的(但不保证哪个线程的写入最终生效)
    }
}

测试2:long赋值的原子性(64位JVM)

public class LongAtomicity64BitJVM {
    private long value = 0L;
    
    public static void main(String[] args) throws InterruptedException {
        LongAtomicity64BitJVM obj = new LongAtomicity64BitJVM();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                obj.value = 0xFFFFFFFFFFFFFFFFL;  // 全1
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                obj.value = 0x0000000000000000L;  // 全0
            }
        });
        
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                long v = obj.value;
                // 在64位JVM上,不会出现混合值
                if (v != 0L && v != 0xFFFFFFFFFFFFFFFFL) {
                    System.out.println("检测到非原子操作:" + Long.toHexString(v));
                }
            }
        });
        
        t1.start();
        t2.start();
        t3.start();
        
        t1.join();
        t2.join();
        t3.join();
        
        System.out.println("测试完成,64位JVM下long赋值是原子的");
    }
}

不同场景下的原子性保证

场景1:局部变量

public void method() {
    int a = 1;  // ✅ 原子操作
    // 局部变量在栈上,线程私有,不存在并发问题
}

场景2:实例变量

public class Instance {
    private int value = 0;
    
    public void setValue(int v) {
        value = v;  // ✅ 单次写入是原子的
                    // ⚠️ 但不保证可见性(其他线程可能看不到)
    }
}

场景3:静态变量

public class Static {
    private static int value = 0;
    
    public static void setValue(int v) {
        value = v;  // ✅ 单次写入是原子的
                    // ⚠️ 但不保证可见性
    }
}

场景4:数组元素

int[] array = new int[10];
array[0] = 1;  // ✅ 单个元素的赋值是原子的

// 但多个元素的赋值不是原子的
array[0] = 1;  // 操作1
array[1] = 2;  // 操作2
// 两个操作之间可能被中断

原子性 ≠ 线程安全

常见误区

public class MisunderstandingAtomicity {
    private int count = 0;
    
    // 错误理解:以为count++是原子的
    public void increment() {
        count++;  // ❌ 非原子操作!
    }
    
    // count++实际上是三个操作:
    // 1. 读取count的值
    // 2. 加1
    // 3. 写回count
    // 只有第3步(单次写入)是原子的,整体不是原子的
}

正确的线程安全实现

public class ThreadSafeCounter {
    private int count = 0;
    
    // 方案1:synchronized
    public synchronized void increment() {
        count++;  // 整体原子性
    }
    
    // 方案2:AtomicInteger
    private AtomicInteger atomicCount = new AtomicInteger(0);
    public void incrementAtomic() {
        atomicCount.incrementAndGet();  // CAS保证原子性
    }
}

性能影响

对齐与非对齐访问

public class AlignmentPerformance {
    // 对齐访问(快)
    @sun.misc.Contended
    static class Aligned {
        int value;  // 自然对齐到4字节边界
    }
    
    // 非对齐访问(慢,但现代JVM会自动对齐)
    static class Unaligned {
        byte padding;
        int value;  // 可能不对齐(实际上JVM会优化)
    }
}

性能差异

  • 对齐访问:1个CPU周期
  • 非对齐访问:可能需要2个CPU周期(跨缓存行)

volatile的开销

public class VolatileOverhead {
    private int normalInt = 0;
    private volatile int volatileInt = 0;
    
    @Benchmark
    public void writeNormal() {
        normalInt = 1;  // ~1ns
    }
    
    @Benchmark
    public void writeVolatile() {
        volatileInt = 1;  // ~10ns(有内存屏障开销)
    }
}

JVM实现差异

JVM类型 int赋值 long赋值 volatile long
32位JVM ✅ 原子 ⚠️ 可能非原子 ✅ 原子
64位JVM ✅ 原子 ✅ 原子 ✅ 原子
Android ART ✅ 原子 ✅ 原子 ✅ 原子

说明

  • 现代64位JVM已成为主流,long/double的原子性问题已基本消失
  • 但在编写跨平台代码时,仍建议对long/double使用volatile

答题总结

分场景回答

对于32位及以下类型(int、short、byte等)

  • ✅ 单次赋值是原子操作
  • ✅ CPU单条指令完成
  • ⚠️ 但不保证可见性(可能需要volatile)

对于64位类型(long、double)

  • ✅ 在64位JVM上是原子操作
  • ⚠️ 在32位JVM上可能非原子(需要volatile)
  • ✅ 使用volatile可保证任何平台都是原子的

原子性 ≠ 线程安全

  • int a = 1 是原子的
  • a++ 不是原子的(读-改-写三步)

面试答题模板

int a = 1 这个赋值操作本身是原子性的。

根据Java内存模型规范,对于32位及以下的基本类型(包括int、short、byte、char、float、boolean),单次读写操作都是原子的。这是因为在硬件层面,这些类型的赋值可以通过单条CPU指令完成,而CPU保证单条指令的不可分割性。

但需要注意两点:

第一,原子性不等于线程安全。虽然单次赋值是原子的,但不保证内存可见性,其他线程可能看不到修改。如果需要可见性保证,应该使用volatile。

第二,对于long和double这两个64位类型,在32位JVM上可能不是原子的,因为可能需要两次32位操作。在64位JVM上,或者使用volatile修饰时,long和double的赋值也是原子的。

所以,int a = 1是原子操作,但如果是a++这种复合操作,就需要使用synchronized或AtomicInteger来保证线程安全。”