返回

Java 中的线程安全:atomic、volatile 和 synchronized 详解

java

Java 中的线程安全:atomic、volatile 和 synchronized

引言

在多线程编程中,协调对共享资源的访问至关重要。Java 提供了 atomic、volatile 和 synchronized 三种关键机制来实现线程安全性,每种机制都有其独特的特点和适用场景。

atomic

atomic 变量确保对单个变量的原子操作,这意味着该变量只能一次更改一个线程。atomic 类型的一个示例是 AtomicInteger,它提供了一个原子递增 (incrementAndGet) 操作。使用 atomic 类型可以确保变量的更新对于所有线程都是可见的,并且不会出现数据竞争。

volatile

volatile 变量强制对变量进行每次读取和写入操作的内存屏障操作。这意味着每次访问 volatile 变量时,都会强制 JVM 将该变量的值从主内存刷新到工作内存,或从工作内存刷新到主内存。volatile 变量非常适合在多线程场景中需要确保变量可见性的情况,但无法保证对该变量的原子操作。

synchronized

synchronized 可用于保护代码块或方法。当一个线程进入一个 synchronized 块或方法时,它会获取该块或方法的对象锁。该锁确保在任何时刻只有一个线程可以执行该代码块或方法。synchronized 块或方法内的所有变量都是对该线程独占的,并且不会出现数据竞争。

比较

特性 atomic volatile synchronized
原子性
可见性
排他性
性能
适用场景 变量更新需要原子性 变量更新需要可见性 确保线程安全和互斥访问

示例

代码 1:非线程安全的计数器

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

此代码不是线程安全的,因为多个线程可能会同时执行 getNextUniqueIndex() 方法,从而导致 counter 变量出现数据竞争。

代码 2:使用 atomic 实现线程安全的计数器

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

此代码是线程安全的,因为 AtomicInteger 提供了对 counter 变量的原子递增操作。这确保了在任何时刻只有一个线程可以修改 counter 变量,从而防止数据竞争。

volatile 的局限性

volatile 变量不提供对变量的原子操作。因此,下述代码:

volatile int i = 0;
void incIBy5() {
    i += 5;
}

并不等同于:

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

后者通过将 i 包装在一个 Integer 对象中并使用 synchronized 块来确保对 i 的原子操作。

synchronized 的实现

synchronized 块或方法的内部实现使用 JVM 监视器,这些监视器负责协调对共享对象的访问并防止数据竞争。当一个线程进入一个 synchronized 块或方法时,它会获取监视器对象上的锁。只有当该线程释放锁时,其他线程才能进入该块或方法。

atomic.incrementAndGet() 的实现

atomic.incrementAndGet() 方法使用了一种称为 CAS (Compare-And-Swap) 的技术。CAS 操作通过比较预期值和实际值来原子更新变量的值。如果预期值与实际值匹配,则更新操作成功执行。否则,CAS 操作将失败,并且该线程将重新尝试该操作。

结论

atomic、volatile 和 synchronized 是 Java 中实现线程安全的三种关键机制。每种机制都有其独特的特点和适用场景,选择合适的机制对于避免数据竞争和确保多线程程序的正确性至关重要。

常见问题解答

  • atomic 和 synchronized 哪个更好?
    • 这取决于具体场景。如果需要原子性,atomic 更适合;如果需要互斥访问,synchronized 更合适。
  • volatile 能保证原子性吗?
    • 不能,volatile 只保证可见性,不保证原子性。
  • synchronized 会阻塞线程吗?
    • 是的,synchronized 会阻塞尝试获取锁的线程,直到锁被释放。
  • atomic.incrementAndGet() 是如何实现原子性的?
    • 使用 CAS (Compare-And-Swap) 技术。
  • 如何避免使用 volatile 变量时的指令重排序?
    • 使用 volatile 和 final 关键字,或通过 volatile 变量访问其他 final 变量来避免指令重排序。