竹笋

首页 » 问答 » 问答 » 为什么局部变量是线程安全的
TUhjnbcbe - 2025/2/15 18:44:00

我们一遍一遍重复讲到,多个线程同时访问共享变量时,会导致并发问题,那在Java语言里,是不是所有的变量都是共享变量呢?工作中我发现不少同学会给方法里的局部变量设置同步,显然这些同学并没有把共享变量说清楚。那么Java方法里的局部变量是否存在并发问题呢?

其实很多人都知道局部变量是不存在数据竞争的,至于原因嘛,就说不清楚了。

那它背后的原因到底是什么样的呢?要弄清楚这个,你需要一点编译原理的知识,你知道在CPU层面,是没有方法概念的。CPU的眼里只有一条条的指令。编译程序,负责把高级语言里的方法转换成一条条的指令。所以你可以站在编译器实现的角度来思考,怎么完成方法到指令的转换?

方法是如何被执行的

高级语言的普通语句里,翻译成CPU的指令相对简单,可方法的调用就比较复杂了。例如下面这三行代码:第1行,声明一个int变量a;第2行,调用方法fibonacci(a);第3行,将b赋值给c。

inta=7;int[]b=fibonacci(a);int[]c=b;

当你调用fibonacci(a)的时候,CPU要先找到方法fibonacci()的地址,然后跳转到这个地址去执行代码,最后CPU执行完方法fibonacci()之后,要能够返回。首先找到调用方法的下一条语句地址,:也就是int[]c=b;的地址,再跳转到这个地址去执行。你可以参考下面这个图再加深一下理解。

到这里,方法调用的过程你就清楚了,但还有一个重要问题,“CPU去哪里找到调用方法的参数和返回地址?”如果你熟悉CPU的工作原理,你应该立刻会想到:通过CPU的堆栈寄存器。CPU支持一种栈结构,先入后出。因为这个栈是和方法调用相关的,所以叫做调用栈。

例如,有三个方法A、B、C,他们的调用关系是A-B-C(A调用B,B调用C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。

利用栈结构来支持方法调用这个方案非常普遍,以至于CPU里内置了栈寄存器。虽然各家编程语言定义的方法千奇百怪,但是方法的内部执行原理却是出奇的一致:都是靠栈结构解决的。Java语言虽然是靠虚拟机解释执行的,但是方法的调用也是利用栈结构解决的。

局部变量存哪里?

我们已经知道方法的调用在CPU眼里是怎么执行的。但还有一个关键问题:方法内的局部变量存哪里?

局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了。局部变量应该和方法共生死。此时你应该会想到调用栈的栈帧,调用栈的栈帧就是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。事实上,的确是这样的,局部变量就是放到了调用栈里。于是调用栈的结构就变成了下图这样。

这个结论相信很多人都知道,因为学Java语言的时候,基本所有的教材都会告诉你new出来的对象是在堆里,局部变量是在栈里,只不过很多人并不清楚堆和栈的区别,以及为什么要区分堆和栈。现在你应该很清楚了,局部变量是和方法同生共死的,一个变量如果想跨越方法的边界,就必须创建在堆里。

调用栈与线程

两个线程可以同时用不同的参数调用相同的方法。那调用栈跟线程是一种什么关系呢?答案:每个线程都有自己独立的调用栈。如果不是这样,那么线程就相互干扰了,如下图所示:

现在,让我们回过头来再看篇首的问题:Java方法里面的局部变量是否存在并发问题?现在你应该很清楚了,一点问题都没有。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。再次重申一遍:没有共享,就没有伤害。

线程封闭

方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经称为解决并发问题的一种途径。同时还有个响亮的名字叫做线程封闭。

即仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会出现并发问题。

采用线程封闭技术的案例非常多,例如从数据库连接池里面获取的Connection,在JDBC规范里并没有要求这个Connection必须是线程安全的。数据库连接池通过线程封闭技术,保证一个Connection一旦被一个线程获取之后,在这个线程关闭Connection之前的这段时间里,不会再分配给其他线程,从而保证了Connection不会有并发问题。

总结

调用栈是一个通用的计算机概念,所有的编程语言都会涉及。

1
查看完整版本: 为什么局部变量是线程安全的