问题背景
在并发编程中,多线程同时访问共享变量往往会导致数据竞争和并发问题。但并非所有变量都会成为共享变量。比如下面这个计算斐波那契数列的方法,当多个线程同时调用时,方法内部的数组r是否存在并发风险?
int[] fibonacci(int n) { // 创建结果数组 int[] r = new int[n]; // 初始化第一、第二个数 r[0] = r[1] = 1; // ① // 计算2..n for(int i = 2; i < n; i++) { r[i] = r[i-2] + r[i-1]; } return r; }很多开发者都知道局部变量是线程安全的,但未必清楚背后的原理。本文从 JVM 和编译原理的角度,梳理局部变量安全的根本原因。
原理分析
方法在 CPU 层面的执行过程
在 CPU 层面,并不存在"方法"的概念,只有一条条指令。编译器负责将高级语言中的方法转换为指令序列。以以下三行代码为例:
int a = 7; int[] b = fibonacci(a); int[] c = b;当调用fibonacci(a)时,CPU 需要先找到该方法的地址并跳转执行,执行完后还需返回调用处的下一条指令地址(即int[] c = b的地址)。这个调用过程可以用下图理解:
这里就产生了一个关键问题:CPU 去哪里寻找调用方法的参数和返回地址?答案是 CPU 内置的堆栈寄存器,它支持一种栈结构——调用栈。
假设有 A、B、C 三个方法,调用关系为 A -> B -> C,运行时就会构建出对应的调用栈。每个方法在调用栈中都有自己的独立空间,称为栈帧,栈帧中保存了该方法所需的参数和返回地址。调用方法时创建新的栈帧并压入调用栈,方法返回时对应栈帧自动弹出,两者同生共死。
局部变量存在哪里
局部变量的作用域是方法内部,方法执行完毕局部变量即失效,这与栈帧的生命周期完全一致,因此局部变量被保存在栈帧中。
在 Java 中,new出来的对象存放在堆中,而局部变量存放在栈中。从生命周期角度很容易理解:局部变量与方法同生共死,而一个变量若想跨越方法的边界,就必须创建在堆中。
调用栈与线程的关系
多个线程可以用不同参数调用同一个方法,它们之间的调用栈互不干扰。每个线程都有自己独立的调用栈,否则线程之间会互相干扰。
回到最初的问题:Java 方法内部的局部变量是否存在并发问题?答案是否定的。每个线程拥有独立的调用栈,局部变量保存在各自的调用栈中,不会被共享,自然不会产生并发问题。
线程封闭
通过局部变量的分析,可以得出一个核心思路:没有共享,就没有伤害。这一思路发展成了并发编程中的重要技术——线程封闭,即仅在单线程内访问数据,由于不存在共享,并发问题自然无从谈起。
总结
- 方法调用的过程:找到方法位置,执行并返回结果。
- CPU 层面没有方法概念,只有指令。
- 每次方法调用创建一个栈帧,栈帧包含局部变量和参数,生命周期与方法一致。
- 每个线程有独立的调用栈,栈帧中的局部变量不会跨线程共享。
常见问题
问:局部变量为什么不会被多个线程共享?答:每个线程都有自己独立的调用栈,局部变量保存在线程各自的调用栈中,不会共享。
问:线程封闭的核心思想是什么?答:仅在单线程内访问数据,由于不存在共享,所以便不会出现并发问题。