写在前面
相信很多小伙伴都知道局部变量是线程安全的, 那你知道为什么局部变量是线程安全的吗?
前言
多个线程同时访问共享变量时, 会导致并发问题. 那么, 如果将变量放在方法内部, 是不是还会存在并发问题呢? 如果不存在并发问题, 那么为什么不会存在并发问题呢?
著名的斐波那契数列
记得上学的时候, 我们都会遇到这样一种题目, 打印斐波那契数列. 斐波那契数列是这样的一个数列: 1,1,2,3,5,8,13,21,34..., 也就是说第 1 项和第 2 项是 1, 从第 3 项开始, 每一项都等于前 2 项之和. 我们可以使用下面的代码来生成斐波那契数列.
- // 生成斐波那契数列
- public int[] fibonacci(int n){
- // 存放结果的数组
- int[] result = new int[n];
- // 数组的第 1 项和第 2 项为 1
- result[0] = result[1] = 1;
- // 计算第 3 项到第 n 项
- for(int i = 2; i < n; i++){
- result[i] = result[i-2] + result[i-1];
- }
- return result;
- }
假设此时有很多个线程同时调用 fibonacci() 方法来生成斐波那契数列, 对于方法中的局部变量 result, 会不会存在线程安全的问题呢? 答案是: 不会!!
接下来, 我们就深入分析下为什么局部变量不会存在线程安全的问题!
方法是如何被执行的?
我们以下面的三行代码为例.
- int x = 5;
- int[] y = fibonacci(x);
- int[] z = y;
当我们调用 fibonacci(x) 时, CPU 要先找到 fibonacci() 方法的地址, 然后跳转到这个地址去执行代码, 执行完毕后, 需要返回并找到调用方法的下一条语句的地址, 也就是 int[] z = y 的地址, 再跳到这个地址去执行. 我们可以将这个过程简化成下图所示.
这里需要注意的是: CPU 会通过堆栈寄存器找到调用方法的参数和返回地址.
例如, 有三个方法 A,B,C, 调用关系为 A 调用 B,B 调用 C. 在运行时, 会构建出相应的调用栈, 我们可以用下图简单的表示这个调用栈.
每个方法在调用栈里都会有自己独立的栈帧, 每个栈帧里都有对应方法需要的参数和返回地址. 当调用方法时, 会创建新的栈帧, 并压入调用栈; 当方法返回时, 对应的栈帧就会被自动弹出.
我们可以这样说: 栈帧是在调用方法时创建, 方法返回时 "消亡".
局部变量存放在哪里?
局部变量的作用域在方法内部, 当方法执行完, 局部变量也就没用了. 可以这么说, 方法返回时, 局部变量也就 "消亡" 了. 此时, 我们会联想到调用栈的栈帧. 没错, 局部变量就是存放在调用栈里的. 此时, 我们可以将方法的调用栈用下图表示.
很多人都知道, 局部变量会存放在栈里. 如果一个变量需要跨越方法的边界, 就必须创建在堆里.
调用栈与线程
两个线程就可以同时用不同的参数调用相同的方法. 那么问题来了, 调用栈和线程之间是什么关系呢? 答案是: 每个线程都有自己独立的调用栈. 我们可以使用下图来简单的表示这种关系.
此时, 我们在看下文中开头的问题: Java 方法内部的局部变量是否存在并发问题? 答案是不存在并发问题! 因为每个线程都有自己的调用栈, 局部变量保存在线程各自的调用栈里, 不会共享, 自然也就不存在并发问题.
线程封闭
方法里的局部变量, 因为不会和其他线程共享, 所以不会存在并发问题. 这种解决问题的技术也叫做线程封闭. 官方的解释为: 仅在单线程内访问数据. 由于不存在共享, 所以即使不设置同步, 也不会出现并发问题!
写在最后
如果觉得文章对你有点帮助, 请微信搜索并关注「 冰河技术 」微信公众号, 跟冰河学习高并发编程技术.
最后, 附上并发编程需要掌握的核心技能知识图, 祝大家在学习并发编程时, 少走弯路.
来源: https://www.cnblogs.com/binghe001/p/12808419.html