为何Java与Scala中子线程修改局部变量的行为存在差异?
Great question! This difference comes down to how each language handles access to local variables from inner classes or anonymous implementations—even though both run on the JVM. Let’s break it down step by step.
Java’s Restriction: Final or Effectively Final Required
In Java, when you access a local variable from an anonymous inner class (like the Runnable instances for your threads), you don’t actually access the original variable directly. Instead, Java creates a copy of the variable’s value and passes that copy to the inner class instance.
To avoid confusion where the original variable and its copy might diverge (leading to buggy, unpredictable behavior), Java enforces that the original variable must be either:
- Explicitly marked
final, or - Effectively final (meaning it’s never modified after its initial assignment).
In your Java code, you’re modifying x inside both threads—this breaks the effectively final rule, so the compiler throws this error:
local variables referenced from an inner class must be final or effectively final
Here’s your Java code for reference:
public class MyClass { static void f() throws Exception { int x = 0; Thread t1 = new Thread(new Runnable() { public void run() { for(int i = 0; i < 1000; i++) { x = x + 1; } } }); Thread t2 = new Thread(new Runnable() { public void run() { for(int i = 0; i < 1000; i++) { x = x - 1; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(x); } public static void main(String args[]) throws Exception { for(int i = 0; i < 20; i++) { f(); } } }
Scala’s Workaround: Behind-the-Scenes Wrapping
Scala takes a more flexible approach to enable this syntax. When you use a var local variable inside an anonymous Runnable (or any inner function/class), Scala automatically wraps the variable in a mutable reference object under the hood. Think of it like a tiny helper class that holds the integer value: the reference to this wrapper is effectively final (so it complies with JVM rules), but the value inside the wrapper can be modified freely.
That’s why your Scala code compiles and runs—you’re not modifying the original local variable itself, but the mutable value inside the shared wrapper. As you noted, though, this introduces a race condition: multiple threads modify the wrapped value without synchronization, so the final output of x will be unpredictable (sometimes 0, sometimes a different number).
Here’s your Scala code for reference:
def f(): Unit = { var x = 0 val t1 = new Thread(new Runnable { override def run(): Unit = (1 to 1000).foreach(_ => {x = x + 1}) }) t1.start() val t2 = new Thread(new Runnable { override def run(): Unit = (1 to 1000).foreach(_ => {x = x - 1}) }) t2.start() t1.join() t2.join() println(x) } (1 to 20).foreach(_ => f())
Key Takeaway
- Java prioritizes safety by preventing situations where copies of local variables could get out of sync, enforcing the final/effectively final rule at compile time.
- Scala prioritizes flexibility, using a hidden wrapper to allow mutable access, but shifts the responsibility of handling thread safety (like using
AtomicIntegeror synchronization) to you, the developer.
内容的提问来源于stack exchange,提问作者Leandro Galvan

