> 文档中心 > Java并发编程 | 局部变量为什么是线程安全的

Java并发编程 | 局部变量为什么是线程安全的


前言

本篇文章说一个比较简单的东西,就是局部变量为什么是线程安全的,对于熟悉JVM的开发者来说这个问题可以忽略不计。所以本篇文章作为知识回顾和扩展。

正文

本系列文章一直说的都是并发编程,即多个线程同时访问共享变量的时候,会导致并发问题,那么在Java语言里,是不是所有变量都会是共享变量呢

局部变量

比如我们看下面这个fibonacci()方法,会根据传入的参数n,返回1到n的斐波那契数列,斐波那契数列数列类似这样:1、1、2、3、5、8...,即第一项和第二项是1,第三项开始,每一项等于前俩项之和,所以方法如下:

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;}复制代码

假如这时有多个线程来调用fibonacci()这个方法时,数组r是否存在数据竞争呢

很多人都知道这里的r是方法的局部变量,是不存在数据竞争的,至于原因,我们就来梳理一下。

方法是如何被执行

想搞明白这个问题,必须了解一些编译原理的知识或者JVM的知识。在CPU层面,是没有方法概念的,在CPU眼里,只有一条条的指令。编译程序,负责把高级语言里的方法转换为一条条指令,所以这时可以站在编译器实现者的角度来思考:怎么完成方法到指令的转换。

我们先来看一下下面的3行代码:

int a = 7;int[] b = fibonacci(a);int[] c = b;复制代码

先声明一个变量a,然后调用fibonacci()方法,将返回值赋值给c;

这3句代码非常简单,当调用fibonacci(a)的时候,CPU要先找到方法fibonacci()的地址,然后跳转到这个地址上去执行代码,最后CPU执行完方法fibonacci()之后,要能够返回。首先找到调用方法的下一条语句的地址:也就是int[] c=b;的地址,再跳转到这个地址去执行。

这个调用过程,下图可以加深理解:

到这里,方法调用的过程基本就清楚了,但是还有一个很重要的问题,即CPU去哪里寻找调用方法的参数和返回地址 这时肯定立即会想到:通过CPU的堆栈寄存器,CPU支持一种栈结构,因为这个栈是和方法调用相关的,因此经常被称为调用栈。

假如有3个方法A、B、C,它们调用的关系是A -> B -> C,在运行时,就会构建出下面的调用栈。每个方法在调用栈里都有自己的独立空间,叫做栈帧,每个栈帧里都有对应方法需要的参数和返回地址。

当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会自动弹出,也就是说栈帧和方法是同生共死的。

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

局部变量存在哪里

我们已经知道了方法在CPU眼里是怎么执行的,那方法的局部变量保存在哪里?

首先我们得知道,局部变量的作用域是方法内部,也就是说方法执行完,局部变量就没用了,即局部变量和方法同生共死。这时就应该联想到和方法同生共死的栈帧,所以把局部变量保存在栈帧中非常合理。

实时上,也是如此,局部变量就是放在了调用栈中,如下图:

在学习Java时,我们都知道new出来的对象在队里,局部变量在栈中,只不过很多人不清楚是为什么 其实就可以从变量的生命周期来思考,局部变量和方法同生共死的,一个变量如果想跨越方法的边界,就必须创建在堆中。

调用栈和线程

2个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢 答案也呼之欲出:每个线程都有自己独立的调用栈。

因为如果不是这样,那2个线程就互相干扰了,如下图所示,线程A B C都有自己独立的调用栈:

现在再来看最开始的问题,Java方法里面的局部变量是否存在并发问题 答案就是没有并发问题,每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈中,不会共享,也就没有并发问题。

线程封闭

前面从局部变量我们可以知道:没有共享,就没有伤害。这个思路就成为了解决并发编程的一个重要技术,叫做线程封闭,即仅在单线程内访问数据,由于不存在共享,所以便不会出现并发问题。

总结

本篇文章解释了局部变量为什么是线程安全的,究其原因就是局部变量没有共享,而在探究这个过程中,我们做个总结:

  1. 方法的执行以及拿到结果,是通过找到方法的位置,执行完拿到执行结果。
  2. 在CPU眼中,是没有方法的概念的,有的只是一个个的指令。
  3. 每调用一次方法,就会创建一个栈帧,栈帧包括局部变量和参数,其生命周期和方法是同生共死的。
  4. 每个线程都有其独立的调用栈,调用栈中是栈帧,所以局部变量不会被共享。