【Java 并发基础】局部变量是线程安全的
⽅法中的变量(即局部变量)是不存在数据竞争(Data Race )的,也是线程安全的。为了理解为什么,我们先来了⼀下⽅法是如何被执⾏的,然后再分析局部变量的安全性,最后再介绍利⽤局部变量不会共享的特点⽽产⽣的解决并发问题的⼀些技术。
int a = 7;
int[] b = fibonacci(a);
int[] c = b;
以上代码转换成CPU 指令执⾏,⽅法的调⽤过程⽰意图如下:(图来⾃参考[1]
当调⽤fibonacci(a)时,CPU 要先到⽅法fibonacci()的地址(在CPU 堆栈寄存器中),然后跳转到这个地址去执⾏代码(蓝⾊线),最后CPU 执⾏完⽅法,再返回原来调⽤⽅法的下⼀条语句(红⾊线)。
CPU 调⽤⽅法的参数和返回地址,是通过堆栈寄存器。CPU ⽀持⼀种线性结构,因为与⽅法调⽤有关,所以也称为调⽤栈。
再举个例⼦,有三个⽅法A 、B 、C 。⽅法A 中调⽤⽅法B ,⽅法B 中调⽤⽅法C 。那么将会构建出如下调⽤栈。每个⽅法在调⽤栈⾥都有⾃⼰的独⽴空间,称为栈帧。每个栈帧都有对应⽅法需要的参数和返回地址。当调⽤新⽅法时,会创建新的栈帧,并压⼊调⽤栈;当⽅法返回
时,对应的栈帧就会被⾃动弹出。即,栈帧和⽅法同⽣共死。
三个⽅法⽣成的调⽤栈如上图所⽰。
不同的编程语⾔虽定义⽅法虽各有所异,但是它们执⾏⽅法的原理却是⼀致的:都是依靠栈结构解决。Java 语⾔虽然是靠虚拟机解释执⾏,但是⽅法的调⽤也是利⽤栈结构解决的。
局部变量是定义在⽅法内,作⽤域也是在⽅法内部。当⽅法运⾏结束后,局部变量也就失效了。那么我们可以得出,局部变量的存放位置应该在调⽤栈中。事实上,局部变量就是存放到调⽤栈中的。前⾔
⽅法是如何被执⾏的
局部变量的存放位置
java多线程入门
两个线程可以同时⽤不同的参数调⽤相同的⽅法,那么调⽤栈和线程之间是什么关系呢?答案就是:每个线程都有⾃⼰独⽴的调⽤栈。所以,Java 当多线程访问没有同步的可变共享变量时就会出现并发问题,⽽解决⽅案之⼀便是使变量不共享。变量不会和其他变量共享,也就不会存在并发问题。仅在单线程⾥访问数据,不需要同步,我们称之为线程封闭。当某个对象封闭在⼀个线程中时,这种⽤法将⾃动实现线程安全性,即使被封闭的对象本⾝不是线程安全的。
采⽤线程封闭技术的案例⾮常多。例如⼀种常见的应⽤便为JDBC 的Connection 对象。从数据库连接池中获取⼀个Connection 对象,在JDBC 规范中并没有要求这个Connection ⼀定是线程安全的。数据库连接池通过线程封闭技术,保证⼀个Connection 对象⼀旦被⼀个线程获取之后,在这个Connection 对象返回之前,连接池不会将它分配给其他线程,从⽽保证了Connection 对象不会有并发问题。
线程封闭技术的⼀个具体实现是我们上⾯提到的局部变量的使⽤(栈封闭),还有⼀种需要提⼀下,即ThreadLocal 类。
ThreadLoacl 类
维持线程封闭性⼀种更规范⽅法是使⽤ThreadLocal ,这个类能使线程中的某个值与保存值的对象相关联起来。ThreadLocal 提供了get()和set()等访问接⼝,这些⽅法为每个使⽤该变量的线程都存有⼀份独⽴的副本,因此get()总是返回由当前执⾏线程在调⽤set()时设置的最新值。
ThreadLocal 对象通常⽤于防⽌对可变的单实例变量(Singleton )或全局变量进⾏共享。
例如,在单线程应⽤程序中可能会维持⼀个全局的数据库连接,并在线程启动时初始化这个连接对象,从⽽避免在调⽤每个⽅法时都要传递⼀个Connection 对象。由于JDBC 的连接对象不⼀定线程安全的,因此,当多线程应⽤程序在没有协同的情况下使⽤全局变量时,就不是线程安全的。通过将JDBC 的连接保存到ThreadLocal 对象中,每个线程都会拥有属于⾃⼰的连接。
如以下代码所⽰,利⽤ThreadLocal 来维持线程的封闭性:(代码来⾃参考[2])
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {                Connection(DB_URL);调⽤栈与线程
利⽤不共享解决并发问题的技术: 线程封闭
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection, e");
}
};
};
public Connection getConnection() {
();
}
}
当某个频繁执⾏的操作需要⼀个临时对象,例如⼀个缓冲区,⽽同时⼜希望避免在每次执⾏时都重新分
配该临时对象,就可以使⽤这项技术。例如,在Java 5.0之前,String()⽅法使⽤ThreadLocal对象来保存⼀个12字节⼤⼩的缓冲区,⽤于对结果进⾏格式化,⽽不是使⽤共享的静态缓冲区(需要使⽤加锁机制)或者每次调⽤时都分配⼀个新的缓冲区。
⼩结
知道⽅法是如何调⽤的也就明⽩了局部变量为什么是线程安全的。⽅法调⽤会产⽣栈帧,局部变量会放在栈帧的⼯作内存中,线程之间不共享,故不存在线程安全问题。后⾯我们介绍了基于不共享解决并发问题的线程封闭技术,除了不共享这种思想可以解决并发问题,还有两种:使⽤不可变变量和正确使⽤同步机制。
参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械⼯业出版社,2016

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。