核心结论
对于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来保证线程安全。”