You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java并发:已用synchronized为何仍需volatile关键字?

关于Volatile关键字在同步代码中的必要性分析

生产者消费者示例代码

以下代码取自Jeff Friesen所著《Java threads and the concurrency utilities》:

public class PC {
    public static void main(String[] args) {
        Shared s = new Shared();
        new Producer(s).start();
        new Consumer(s).start();
    }
}
class Shared {
    private char c;
    private volatile boolean writeable = true;

    synchronized void setSharedChar(char c) {
        while (!writeable)
            try {
                wait();
            } catch (InterruptedException ie)  {
            }
        this.c = c;
        writeable = false;
        notify();
    }

    synchronized char getSharedChar() {
        while (writeable)
            try {
                wait();
            } catch (InterruptedException ie)  {
            }
        writeable = true;
        notify();
        return c;
    }
}
class Producer extends Thread {
    private final Shared s;

    Producer(Shared s) {
        this.s = s;
    }

    @Override
    public void run() {
        for (char ch = 'A'; ch <= 'Z'; ch++) {
            synchronized(s) {
                s.setSharedChar(ch);
                System.out.println(ch + " produced by producer.");
            }
        }
    }
}
class Consumer extends Thread {
    private final Shared s;

    Consumer(Shared s) {
        this.s = s;
    }

    @Override
    public void run() {
        char ch;
        do {
            synchronized(s) {
                ch = s.getSharedChar();
                System.out.println(ch + " consumed by consumer.");
            }
        } while (ch != 'Z');
    }
}

疑问1

Shared类中的writeable字段仅在synchronized方法中被访问,而synchronized具备与volatile相同的可见性及写互斥特性,为何仍需将其声明为volatile?同时为何writeable需要volatile修饰,而c字段不需要?


银行账户示例代码

public class CheckingAccount {
    private volatile int balance;

    public CheckingAccount(int initialBalance) {
        balance = initialBalance;
    }

    public synchronized boolean withdraw(int amount) {
        if (amount <= balance) {
            try {
                Thread.sleep((int) (Math.random() * 200));
            } catch (InterruptedException ie) {
            }
            balance -= amount;
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        final CheckingAccount ca = new CheckingAccount(100);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++)
                    System.out.println (name + " withdraws $10: " +
                ca.withdraw(10));
            }
        };
        Thread thdHusband = new Thread(r);
        thdHusband.setName("Husband");
        Thread thdWife = new Thread(r);
        thdWife.setName("Wife");
        thdHusband.start();
        thdWife.start();
    }
}

疑问2

balance字段仅在synchronized的withdraw方法中被访问,为何也需声明为volatile?


专业解释

一、生产者消费者场景中writeable的volatile必要性

首先明确:线程调用wait()方法时,会主动释放锁并进入等待状态;被其他线程唤醒后,必须重新获取锁才能继续执行。

在Shared类的同步方法里,writeable是循环等待的核心条件:生产者要等writeable为true才能写入,消费者要等writeable为false才能读取。这里的关键问题是:线程被唤醒后,重新获取锁之前,能不能看到writeable的最新值?

虽然进入synchronized块能保证看到变量最新值,但线程被唤醒后到重新拿到锁之间,writeable可能已经被其他线程修改。如果writeable没加volatile,JVM可能把它缓存到线程本地内存,导致唤醒后的线程看到旧值,直接进入无限等待的死循环——这就是必须加volatile的原因:它能保证writeable的修改立即同步到主内存,线程读取时直接从主内存拿最新值,避免缓存不一致。

而c字段不需要volatile,是因为它的所有读写都在synchronized块内完成,且和writeable的状态强绑定:只有writeable状态切换后,c才会被读写,同步块已经完全保证了c的可见性和原子性,没必要多此一举加volatile。

二、银行账户场景中balance的volatile必要性

这个场景里,balance加volatile其实是多余的,属于过度防御。

因为所有对balance的读写操作都在synchronized的withdraw方法里:进入同步方法时,线程会强制从主内存读取balance的最新值;退出同步方法时,会把balance的修改刷新到主内存。同步机制已经完全覆盖了volatile的可见性和原子性保障,加volatile不会带来任何额外收益,反而容易误导代码的设计意图。

当然,如果未来有非同步方法访问balance,volatile能提前保证可见性,但从当前代码来看,这种情况不存在,所以volatile是不必要的。


内容的提问来源于stack exchange,提问作者rico

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 03:13:12