Java并发11:局部变量
方法执行
int a = 7;
int[] b = fibonacci(a);
int[] c = b;
执行第2行时,CPU会找到fibonacci()
的地址,跳转到该地址执行代码,执行完返回。
找到调用方法下一行,也就是第3行的地址去执行。
CPU通过其堆栈寄存器找到调用方法的参数和返回地址,栈是和方法相关的,被称为调用栈。
堆栈寄存器指向栈顶内存地址。
如图3个方法A-B-C的调用关系,运行时会构造出以下形式调用栈,每个方法有自己独立的空间,称为栈帧。
每个栈帧都有对应方法需要的参数和返回地址。
调用方法时,会创建新栈帧,并压入调用栈;
方法返回时,对应栈帧被自动弹出。即栈帧和方法是同生共死的。
栈结构支持方法调用这个方案非常普遍,以至于 CPU 里内置了栈寄存器。
各编程语言方法的内部执行原理出奇一致:都是靠栈结构解决的。
Java 虽然是靠虚拟机解释执行的,但是方法的调用也是利用栈结构解决的。
先计算参数
方法调用时会先计算参数,再执行方法体。
while(idx++ < 10000) {
//先执行get()+1
set(get()+1);
}
例如写日志的代码,如果日志级别设置为 INFO,虽然这行代码不会写日志,但是会计算”The var1:” + var1 + “, var2:” + var2的值,因为方法调用前会先计算参数。
logger.debug("The var1:" + var1 + ", var2:" + var2);
更好的写法:
logger.debug("The var1:{}, var2:{}", var1, var2);
这种写法仅将参数压栈,而没有参数的计算。使用{}占位符是写日志的一个良好习惯。
局部变量的位置
局部变量作用域是方法内部,方法执行完,局部变量就失效了。和方法也是同生死。
一个变量如果想跨越方法的边界,就必须创建在堆里。
调用栈与线程
每个线程都有自己独立的调用栈
线程的调用栈独立,方法的栈帧独立,局部变量只存在于对应方法的栈帧中,因此无并发问题。
线程封闭
线程封闭:即仅在单线程内访问数据,作为解决并发问题的一种手段。
例如从数据库连接池里获取的连接 Connection,在JDBC 规范并未要求 Connection 必须是线程安全的。
数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题。
思考
递归调用太深,可能导致栈溢出。原因是什么?有哪些解决方案?
不断开辟栈帧导致内存空间不足。
不使用递归,循环替代/减少递归次数/尾递归。
所有的递归算法都可以用非递归算法实现。
别人的总结
栈溢出原因:
因为每调用一个方法就会在栈上创建一个栈帧,方法调用结束后就会弹出该栈帧,而栈的大小不是无限的,所以递归调用次数过多的话就会导致栈溢出。而递归调用的特点是每递归一次,就要创建一个新的栈帧,而且还要保留之前的环境(栈帧),直到遇到结束条件。所以递归调用一定要明确好结束条件,不要出现死循环,而且要避免栈太深。
解决方法:
- 简单粗暴,不要使用递归,使用循环替代。缺点:代码逻辑不够清晰;
- 限制递归次数;
- 使用尾递归,尾递归是指在方法返回时只调用自己本身,且不能包含表达式。编译器或解释器会把尾递归做优化,使递归方法不论调用多少次,都只占用一个栈帧,所以不会出现栈溢出。然鹅,Java没有尾递归优化。
——uyong
对于这句话:“ new 出来的对象是在堆里,局部变量在栈里”
我觉得应该是对象在堆里,引用(句柄)在栈里。——suynan
note:方法中new出来的对象属于局部变量:对象在堆里,但是指针在栈里。
如果方法内部又有多线程,那方法内部的局部变量是不是也不是线程安全。
——Xiao
方法内部的局部变量传到线程里要是final修饰的,不会有这种问题。
——Carisy
其实就是并发调用方法而产生的局部变量指向的内存地址都是不同的。
所以同一时刻只会有一个线程去操作这些局部变量指向的内存。
——crudBoy