编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

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
      image

2.3 理解调用栈设计

  • CPU去哪里找到调用方法的参数和返回地址?
    • 如果你熟悉 CPU 的工作原理,你应该会立刻想到:通过 CPU 的堆栈寄存器。
    • CPU 支持一种栈结构,栈你一定很熟悉了,就像手枪的弹夹,先入后出。因为这个栈是和方法调用相关的,因此经常被称为调用栈。
  • 有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。
    • 每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。
    • 当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。
    • image
      image
    • 利用栈结构来支持方法调用这个方案非常普遍,以至于 CPU 里内置了栈寄存器。虽然各家编程语言定义的方法千奇百怪,但是方法的内部执行原理却是出奇的一致:都是靠栈结构解决的。

2.4 局部变量存哪里

  • 方法内的局部变量存哪里?
    • 局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了,局部变量应该和方法同生共死。
    • 此时你应该会想到调用栈的栈帧,调用栈的栈帧就是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。
    • 事实上,的确是这样的,局部变量就是放到了调用栈里。
  • 调用栈的结构如下图这样
    • image
      image
    • 基本所有的教材都会告诉你 new 出来的对象是在堆里,局部变量是在栈里,为什么要区分堆和栈。局部变量是和方法同生共死的,一个变量如果想跨越方法的边界,就必须创建在堆里。

2.5 调用栈与线程关系

  • 两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?
    • 答案是:每个线程都有自己独立的调用栈。因为如果不是这样,那两个线程就互相干扰了。
    • 如下面这幅图所示,线程 A、B、C 每个线程都有自己独立的调用栈。
    • image
      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),用于存储方法的局部变量、方法参数和返回地址等信息。
    • 当方法执行完毕后,对应的栈帧会被弹出,恢复到上一个方法的执行状态。
  • 全局变量不会存储在调用栈中
    • 因为全局变量不是方法的局部变量,而是属于类的变量。全局变量在类加载时被初始化,并且在整个程序的生命周期内都存在,无需依赖方法的调用和执行。
  • 每个线程都有自己的调用栈,用于跟踪线程的方法调用和执行
    • 全局变量在多线程环境下可以被多个线程同时访问,但它们的值在不同线程之间是共享的。
    • 因此,在多线程环境下,对全局变量的访问需要考虑线程安全性和同步机制,以避免竞态条件和数据不一致的问题。
贡献者: yangchong211
上一篇
6.3线程安全如何保证
下一篇
6.5并发上下文切换原理