Java多线程编程之慎重使用volatile关键字
在Java多线程环境中,开发者常依赖 volatile 关键字解决可见性问题,但误将其用于复合操作(如自增)会导致严重的数据不一致。本文将深入解析为何 volatile 不能保证原子性,并通过代码对比展示如何使用 synchronized 正确实现线程安全的计数操作。
volatile 关键字的核心作用与局限
volatile 是 Java 虚拟机提供的轻量级同步机制,主要用于保证变量的 可见性 和禁止指令重排序。对于简单的读写操作(如直接赋值),volatile 确实能确保所有线程看到的都是最新值。然而,许多开发者错误地认为它为变量提供了“原子级别”的操作保护,这在变量操作依赖当前值时会失效。
案例演示:为何 n = n + 1 不是原子操作
考虑以下场景:我们创建一个共享计数器 n,声明为 volatile,并启动多个线程对其进行累加操作。如果 volatile 能保证原子性,最终结果应为预期值;但由于读、改、写的过程被拆分,实际结果往往小于预期。
第1步:构建测试环境
创建一个继承自 Thread 的类,定义静态 volatile int n = 0。在主方法中启动多个线程执行累加任务。
package mythread;
public class JoinThread extends Thread {
public static volatile int n = 0;
public void run() {
for (int i = 0; i < 10; i++)
try {
n = n + 1; // 非原子操作
sleep(3); // 增加冲突概率
} catch (Exception e) {}
}
public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[100];
// 创建线程
for (int i = 0; i < threads.length; i++)
threads[i] = new JoinThread();
// 启动线程
for (int i = 0; i < threads.length; i++)
threads[i].start();
// 等待所有线程结束
for (int i = 0; i < threads.length; i++)
threads[i].join();
System.out.println("n=" + JoinThread.n);
}
}
第2步:观察执行结果
在上述代码中,如果每次迭代的输出结果都等于 1000(100个线程 × 每个线程循环10次),则说明操作是原子的。但在实际运行中,输出值通常明显小于 1000。这是因为 n = n + 1 涉及三个步骤:
1. 读取 n 的当前值。
2. 计算 n + 1。
3. 将新值写回 n。
当多个线程同时执行这三步时,可能出现“丢失更新”现象。例如,线程A读取 n=5,线程B也读取 n=5,两者都计算出 6 并写回,导致总次数只增加了 1 而不是 2。
解决方案:使用 synchronized 保证原子性
要使依赖旧值的操作具有原子性,必须使用互斥锁。synchronized 关键字可以确保同一时刻只有一个线程执行特定代码块或方法,从而消除竞争条件。
第1步:重构代码引入同步
将之前的 volatile 变量移除,改为普通变量,并将累加逻辑封装在 synchronized 修饰的方法中。
package mythread;
public class JoinThread extends Thread {
public static int n = 0; // 移除 volatile
public static synchronized void inc() { // 方法同步
n++;
}
public void run() {
for (int i = 0; i < 10; i++)
try {
inc(); // 调用同步方法
sleep(3);
} catch (Exception e) {}
}
public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++)
threads[i] = new JoinThread();
for (int i = 0; i < threads.length; i++)
threads[i].start();
for (int i = 0; i < threads.length; i++)
threads[i].join();
System.out.println("n=" + JoinThread.n); // 现在稳定输出 1000
}
}
第2步:验证执行状态
由于 inc() 方法是同步的,任意时刻只有一个线程能执行 n++。尽管执行时间可能变长,但能保证数据的绝对一致性,最终输出固定为 1000。
总结:何时使用 volatile,何时使用 synchronized?
在使用 volatile 时必须谨慎区分操作的性质:
- 非依赖型赋值:如 n = m + 1(假设 m 来自其他来源且不在此行变更),此操作在当前指令上下文中不涉及对 n 旧值的读取修改,此时 volatile 可视为有效且性能更高。
- 依赖型操作:如 n++, n += 1, if(n > 0) 等,这些操作均涉及“读-改-写”序列,volatile 无法提供原子保障。
结论:若不确定操作是否真正原子,或涉及复杂的状态变更,请优先使用 synchronized 或 java.util.concurrent 包中的工具类(如 AtomicInteger),切勿盲目信任 volatile 的安全性。
以上就是Java多线程编程中关于volatile关键字的详细解析与使用建议,更多关于Java并发编程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













