Java多线程中volatile与synchronized的区别与应用场景
在Java并发编程中,正确理解并运用volatile与synchronized是解决线程安全问题的核心。许多开发者误认为给共享变量加上volatile关键字就能实现完全同步,但实际测试表明,对于包含读-改-写操作的复合语句(如count++),volatile仅保证变量的可见性,却无法保证操作的原子性,导致数据竞争。本文将通过具体代码验证两者的行为差异,并明确各自的适用边界。
多线程基础场景:count++的非线程安全性
首先看一个典型的非线程安全案例。我们创建一个继承自Thread的类,其中包含一个静态整型变量count,并在run方法中对它执行count++操作。在主函数中启动10000个线程,期望最终输出结果为10000。
package com.chzhao;
public class Volatiletest extends Thread {
private static int count = 0;
public void run() {
count++;
}
public static void main(String[] args) {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Volatiletest();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
由于count++不是原子操作(它由读取、自增、写入三个步骤组成),且多个线程可能同时读取到相同的值进行修改,因此最终输出结果通常会小于10000,出现数据丢失现象。
尝试使用volatile解决:为何无效?
为了解决上述问题,直觉上可能会想到使用volatile关键字来修饰count,因为它常被提及用于“同步”场景。修改后的代码如下:
package com.chzhao;
public class Volatiletest extends Thread {
private volatile static int count = 0;
public void run() {
count++;
}
public static void main(String[] args) {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Volatiletest();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
尽管添加了volatile,多次运行后发现输出结果依然经常不是10000。这说明volatile并不能保证count++这类复合操作的线程安全。
使用synchronized解决:确保原子性
要确保线程安全,必须引入锁机制来保证操作的原子性。使用synchronized关键字对修改共享变量的代码块进行同步控制:
package com.chzhao;
public class SynchronizedTest extends Thread {
private static int count = 0;
public void run() {
synchronized (LockClass.lock) {
count++;
}
}
public static void main(String[] args) {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new SynchronizedTest();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
class LockClass {
public static byte[] lock = new byte[0];
}
在此模式下,所有线程在修改count前必须先获取LockClass.lock对象锁,从而确保同一时刻只有一个线程能执行count++。再次运行程序,输出结果将稳定为10000,证明了synchronized的有效性。
核心结论与最佳实践
Java语言提供了两种主要的同步机制:synchronized块/方法和volatile变量。二者的核心区别如下:
- visibility(可见性):
volatile和synchronized都能保证变量的可见性,即一个线程修改了变量,其他线程能立即看到最新值。 - atomicity(原子性):只有
synchronized(以及java.util.concurrent.locks中的显式锁)能保证复合操作的原子性。volatile不保证原子性,这是导致上述例子失败的根本原因。 - performance(性能):
volatile的实现机制更简单,运行时开销通常低于synchronized。但在现代JVM中,随着锁优化技术的发展,轻量级锁等机制已大大缩小了二者在简单场景下的性能差距。
因此,volatile并非“没用”,而是有其特定的适用范围。它适用于满足以下条件之一的场景:
- 状态标志位(如
boolean done)。 - 单次写入、多次读取的配置变量(前提是变量本身不变或不可变)。
- 双重检查锁定(Double-Checked Locking)模式中的实例引用。
而对于涉及状态依赖或复合操作(如递增、累加、条件判断后修改)的共享变量,必须使用synchronized或ReentrantLock等同步手段。
以上就是Java多线程中volatile和synchronized用法的详细内容,更多关于Java并发编程技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













