6.4变量的线程安全探索
目录介绍
- 01.如何设计变量安全性
- 1.1 局部变量保证安全
- 1.2 线程封闭保证安全
- 1.3 使用同步保证变量安全
- 1.4 原子变量保证安全
- 1.5 使用不可变对象
- 1.6 避免变量共享可变状态
- 02.局部变量线程安全
- 2.1 思考局部变量安全问题
- 2.2 方法如何被执行
- 2.3 理解调用栈设计
- 2.4 局部变量存哪里
- 2.5 调用栈与线程关系
- 2.6 没有共享无并发
- 03.全局变量线程不安全
- 3.1 全局变量为何不安全
- 3.2 全局变量存哪里
- 3.3 全局变量有调用栈吗
01.如何设计变量安全性
1.1 局部变量保证安全
- 将变量声明为方法内的局部变量,而不是共享的全局变量。
- 局部变量在每个线程中都有自己的副本,因此不会出现线程安全问题。
1.2 线程封闭保证安全
- 变量限制在单个线程内部,确保每个线程都只能访问自己的变量副本。
- 例如,可以将变量作为方法参数传递给线程的run()方法,或者使用ThreadLocal类来存储线程本地变量。
1.3 使用同步保证变量安全
- 使用synchronized关键字或其他同步机制来保护共享变量的访问。
- 同步可以确保在同一时间只有一个线程可以访问共享变量,从而避免竞态条件和数据不一致的问题。
1.4 原子变量保证安全
- 使用java.util.concurrent.atomic包中的原子变量类,如AtomicInteger、AtomicLong等。
- 原子变量提供了一些原子操作,可以确保对变量的读取和修改是原子的,从而避免竞态条件。
1.5 使用不可变对象
- 设计不可变的对象,即对象的状态在创建后不能被修改。
- 不可变对象是线程安全的,因为它们不会发生状态的改变。
1.6 避免变量共享可变状态
- 尽量避免多个线程共享可变状态的情况。
- 如果必须共享可变状态,确保对共享状态的访问是同步的或通过其他线程安全的方式进行。
02.局部变量线程安全
2.1 思考局部变量安全问题
- Java 方法里面的局部变量是否存在并发问题呢?
- 代码里的 fibonacci() 这个方法,会根据传入的参数 n ,返回 1 到 n 的斐波那契数列,类似这样: 1、1、2、3、5、8、13、21、34……每一项都等于前两项之和。
- 在这个方法里面,有个局部变量:数组 r 用来保存数列的结果,每次计算完一项,都会更新数组 r 对应位置中的值。
- 你可以思考这样一个问题,当多个线程调用 fibonacci() 这个方法的时候,数组 r 是否存在数据竞争呢?局部变量不存在数据竞争的!
public class Test{ // 返回斐波那契数列 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; } }
2.2 方法如何被执行
- 方法是如何被执行的
- 例如上面的r【i】 = r【i-2】 + r【i-1】;翻译成 CPU 的指令相对简单,可方法的调用就比较复杂了。
- 例如下面这三行代码:第 1 行,声明一个 int 变量 a;第 2 行,调用方法 fibonacci(a);第 3 行,将 b 赋值给 c。
int a = 7; int[] b = fibonacci(a); int[] c = b;
- 当你调用 fibonacci(a) 的时候,CPU 要先找到方法 fibonacci() 的地址,然后跳转到这个地址去执行代码,最后 CPU 执行完方法 fibonacci() 之后,要能够返回。
- 首先找到调用方法的下一条语句的地址:也就是int[] c=b;的地址,再跳转到这个地址去执行。
image
2.3 理解调用栈设计
- CPU去哪里找到调用方法的参数和返回地址?
- 如果你熟悉 CPU 的工作原理,你应该会立刻想到:通过 CPU 的堆栈寄存器。
- CPU 支持一种栈结构,栈你一定很熟悉了,就像手枪的弹夹,先入后出。因为这个栈是和方法调用相关的,因此经常被称为调用栈。
- 有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。
- 每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。
- 当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。
image - 利用栈结构来支持方法调用这个方案非常普遍,以至于 CPU 里内置了栈寄存器。虽然各家编程语言定义的方法千奇百怪,但是方法的内部执行原理却是出奇的一致:都是靠栈结构解决的。
2.4 局部变量存哪里
- 方法内的局部变量存哪里?
- 局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了,局部变量应该和方法同生共死。
- 此时你应该会想到调用栈的栈帧,调用栈的栈帧就是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。
- 事实上,的确是这样的,局部变量就是放到了调用栈里。
- 调用栈的结构如下图这样
image - 基本所有的教材都会告诉你 new 出来的对象是在堆里,局部变量是在栈里,为什么要区分堆和栈。局部变量是和方法同生共死的,一个变量如果想跨越方法的边界,就必须创建在堆里。
2.5 调用栈与线程关系
- 两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?
- 答案是:每个线程都有自己独立的调用栈。因为如果不是这样,那两个线程就互相干扰了。
- 如下面这幅图所示,线程 A、B、C 每个线程都有自己独立的调用栈。
image
- Java 方法里面的局部变量是否存在并发问题?
- 现在你应该很清楚了,一点问题都没有。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。
- 再次重申一遍:没有共享,就没有伤害。
2.6 没有共享无并发
- 方法里的局部变量,因为不会和其他线程共享,所以没有并发问题
- 这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个响当当的名字叫做线程封闭。
- 比较官方的解释是:仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会有并发问题,性能杠杠的。
- 采用线程封闭技术的案例非常多
- 例如从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。
- 数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题。
03.全局变量线程不安全
3.1 全局变量为何不安全
- 全局变量(也称为共享变量)在多线程环境下可能会导致线程安全问题,因此被认为是不安全的。
- 竞态条件(Race condition):当多个线程同时访问和修改全局变量时,由于执行顺序的不确定性,可能会导致不可预测的结果。例如,两个线程同时读取并修改全局变量的值,可能会导致数据不一致或丢失更新。
- 内存可见性(Memory visibility):在多线程环境中,每个线程都有自己的工作内存,其中包含了对共享变量的本地副本。当一个线程修改了共享变量的值时,其他线程可能无法立即看到这个修改,因为它们仍然使用自己的本地副本。这可能导致线程之间的数据不一致。
- 并发修改(Concurrent modification):如果多个线程同时对全局变量进行修改,可能会导致数据损坏或丢失。例如,一个线程正在修改全局变量的值,而另一个线程正在读取该值,可能会导致读取到不一致的数据。
- 缺乏同步机制:全局变量通常没有内置的同步机制来保护对其的并发访问。如果没有适当的同步措施,多个线程可能会同时读取和修改全局变量,从而导致线程安全问题。
3.2 全局变量存哪里
- 全局变量——>静态变量,静态变量是属于类的变量,而不是对象的变量。
- 它们在类加载时被初始化,并且在整个程序的生命周期内都存在。静态变量可以通过类名直接访问,因此可以在任何地方使用。
- 静态变量存储在方法区(Method Area)中。
- 全局变量——>实例变量,实例变量是属于对象的变量,每个对象都有自己的实例变量副本。
- 它们在对象创建时被初始化,并且在对象的生命周期内存在。
- 实例变量存储在堆内存(Heap)中。
- 全局变量——>常量,常量是在程序中声明的不可变的变量,其值在声明时被初始化,并且在整个程序的生命周期内保持不变。
- 常量通常使用final关键字进行声明,并且在编译时被存储在常量池(Constant Pool)中。
3.3 全局变量有调用栈吗
- 调用栈是用于跟踪方法调用和方法执行的一种数据结构。
- 每当一个方法被调用时,会在调用栈中创建一个新的栈帧(Stack Frame),用于存储方法的局部变量、方法参数和返回地址等信息。
- 当方法执行完毕后,对应的栈帧会被弹出,恢复到上一个方法的执行状态。
- 全局变量不会存储在调用栈中
- 因为全局变量不是方法的局部变量,而是属于类的变量。全局变量在类加载时被初始化,并且在整个程序的生命周期内都存在,无需依赖方法的调用和执行。
- 每个线程都有自己的调用栈,用于跟踪线程的方法调用和执行
- 全局变量在多线程环境下可以被多个线程同时访问,但它们的值在不同线程之间是共享的。
- 因此,在多线程环境下,对全局变量的访问需要考虑线程安全性和同步机制,以避免竞态条件和数据不一致的问题。