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

局部变量不存在数据竞争

在并发编程领域里,没有共享就没有伤害。对于局部变量是不存在数据竞争的,为什么呢?

比如,下面代码里的 fibonacci() 这个方法,会根据传入的参数 n ,返回 1 到 n 的斐波那契数列,斐波那契数列类似这样: 1、1、2、3、5、8、13、21、34……第 1 项和第 2 项是 1,从第 3 项开始,每一项都等于前两项之和。在这个方法里面,有个局部变量:数组 r 用来保存数列的结果,每次计算完一项,都会更新数组 r 对应位置中的值。你可以思考这样一个问题,当多个线程调用 fibonacci() 这个方法的时候,数组 r 是否存在数据竞争(Data Race)呢?

1
2
3
4
5
6
7
8
9
10
11
12
// 返回斐波那契数列
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的写入读取会发生数据竞争。

方法是如何被执行的

对于CPU来说是没有方法这一层面的,对于它来说任何操作都是一条条指令,那么CPU如果进行一个方法调用总要进行返回到调用方法的代码片段(地址)去执行,这个CPU是怎么做到的呢?

答案就是栈,在我的关于栈的文章中提到过方法栈,CPU就是通过栈来实现返回到原来调用方法的地址的。

方法栈

在线程执行进入方法的时候,会将这个方法的一些信息作为栈帧压入方法栈中。一些信息可能有参数,返回地址(这个肯定是必要的,因为出栈的时候需要用到,不然无法返回了)。但栈帧出栈的时候就意味着这个方法执行完了,所以隐含意思就是一个栈帧对应着一次一个方法的执行,栈帧和一次一个方法的执行是同生死共命运的

而我们也知道,对于局部变量来说,在方法执行完就会消失,所以局部变量和栈帧就有着同样的性质了,即局部变量,栈帧,一次一个方法的执行这三者是同时消亡的。所以把局部变量放入栈帧中是最合适不过的了,现实也是这么做的。这也解释了Java中为什么局部变量是存放在栈中的,想要跨越方法的边界,那么变量就必须放入堆中。

方法栈

线程与方法栈

方法栈

从操作系统层面来说,每个线程不独立拥有资源,但是它还是拥有一些必要的资源比如线程控制块(也不算资源),用户栈,内核栈。。所以线程是拥有自己的栈的,也就是说每个线程执行过程中都拥有自己的方法调用栈,而栈帧是在这个方法栈中的,栈帧里面拥有着局部变量,所以可以说,每个线程的局部变量根本不是一个地址,所以就不会出现数据竞争了,所以局部变量是线程安全的。

线程封闭

因为局部变量不存在并发问题,现在也成为了一个解决并发问题的重要思路了,叫线程封闭

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

-------------本文结束感谢阅读-------------