Java多线程中volatile与synchronized的区别与应用场景

桃夭
桃夭
发布时间:2026-10-14 | 4人浏览过
原创

在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并非“没用”,而是有其特定的适用范围。它适用于满足以下条件之一的场景:

  1. 状态标志位(如boolean done)。
  2. 单次写入、多次读取的配置变量(前提是变量本身不变或不可变)。
  3. 双重检查锁定(Double-Checked Locking)模式中的实例引用。

而对于涉及状态依赖或复合操作(如递增、累加、条件判断后修改)的共享变量,必须使用synchronized或ReentrantLock等同步手段。

以上就是Java多线程中volatile和synchronized用法的详细内容,更多关于Java并发编程技巧的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表