Java并发:已用synchronized为何仍需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

