Java 并发学习笔记(一)——原子性、可见性、有序性问题

简介: 计算机的 CPU、内存、I/O 设备的速度一直存在较大的差异,依次是 CPU > 内存 > I/O 设备,为了权衡这三者的速度差异,主要提出了三种解决办法:• CPU 增加了缓存,均衡和内存的速度差异• 发明了进程、线程,分时复用 CPU,提高 CPU 的使用效率• 编译指令优化,更好的利用缓存三种解决办法虽然有效,但是也带来了另外的三个问题,分别就是并发 bug 产生的源头。

计算机的 CPU、内存、I/O 设备的速度一直存在较大的差异,依次是 CPU > 内存 > I/O 设备,为了权衡这三者的速度差异,主要提出了三种解决办法:

  • CPU 增加了缓存,均衡和内存的速度差异
  • 发明了进程、线程,分时复用 CPU,提高 CPU 的使用效率
  • 编译指令优化,更好的利用缓存

三种解决办法虽然有效,但是也带来了另外的三个问题,分别就是并发 bug 产生的源头。


1.可见性问题


如果是单核 CPU,多个线程操作的都是同一个 CPU 缓存,那么一个线程修改了共享变量,另一个线程肯定能马上看到。

如果是多核 CPU ,每个 CPU 都有自己的缓存,这样线程对共享变量的修改便对其他线程不可见了。


2.原子性问题


为什么会有线程切换?一个线程在执行的过程中,可能会进行耗时的 I/O 操作,这时线程需要等待 I/O 操作完成。线程在等待的过程中,可以释放 CPU 的使用权,让另一个线程执行,这样能够提高 CPU 的使用率。

例如上图,两个线程同时对变量 count 加 1,线程 A 在执行的过程中切换到了线程 B,最后导致写入到内存的值都是 1,与预期不符。


3.有序性问题


首先看一段很经典的获取单例对象的代码:

public class Singleton {
    private static Singleton instance;
     //Java 获取单例对象
    public Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

程序的逻辑是:首先判断 instance 是否为空,如果为空,对其加锁,然后再判断是否为空,此时为空的话则初始化 instance 对象。

如果线程 A 和 B 同时执行方法,在 synchronized 处,一个线程会被阻塞,假设被阻塞的是线程 B,此时线程 A 进入并初始化 instance,然后唤醒线程 B,线程 B 进入的时候,发现 instance 不为空了,所以不会创建对象。

但是因为有序性问题的存在,这段代码也不是想象的那么完美,我们期望的初始化对象的过程是这样的:1.分配内存;2.初始化对象;3.将内存地址赋给 instance。但是经过编译优化之后,却是这样的:

  • 1.分配内存
  • 2.将内存地址赋给 instance
  • 3.在内存上面初始化对象

这样,如果线程 A 执行到了第二步,然后切换到 线程 B,线程 B 就会认为 instance 不为空然后直接返回了,实际上 instance 并没有初始化。

最后,总结一下,导致并发问题的三个源头分别是

  • 原子性:一个线程在执行的过程当中不被中断。
  • 可见性:一个线程修改了共享变量,另一个线程能够马上看到,就叫做可见性。
  • 有序性:编译指令重排导致的问题。
相关文章
|
8月前
|
Java API 微服务
2025 年 Java 从入门到精通学习笔记全新版
《Java学习笔记:从入门到精通(2025更新版)》是一本全面覆盖Java开发核心技能的指南,适合零基础到高级开发者。内容包括Java基础(如开发环境配置、核心语法增强)、面向对象编程(密封类、接口增强)、进阶技术(虚拟线程、结构化并发、向量API)、实用类库与框架(HTTP客户端、Spring Boot)、微服务与云原生(容器化、Kubernetes)、响应式编程(Reactor、WebFlux)、函数式编程(Stream API)、测试技术(JUnit 5、Mockito)、数据持久化(JPA、R2DBC)以及实战项目(Todo应用)。
456 6
|
4月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
136 4
|
4月前
|
缓存 安全 Java
如何理解Java中的并发?
Java并发指多任务交替执行,提升资源利用率与响应速度。通过线程实现,涉及线程安全、可见性、原子性等问题,需用synchronized、volatile、线程池及并发工具类解决,是高并发系统开发的关键基础。(238字)
299 5
|
7月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
402 83
|
10月前
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
7月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
430 83
|
11月前
|
存储 Java
# 【Java全栈学习笔记-U1-day02】变量+数据类型+运算符
本篇笔记主要围绕Java全栈学习的第二天内容展开,涵盖了变量、数据类型、运算符以及Scanner类的应用。首先介绍了变量的概念与命名规范,以及如何定义和使用变量;接着详细讲解了Java中的基本数据类型,包括整型、浮点型、字符型、布尔型等,并通过实例演示了数据类型的运用。随后,深入探讨了各类运算符(赋值、算术、关系、逻辑)及其优先级,帮助理解表达式的构成。最后,介绍了如何利用Scanner类实现用户输入功能,并通过多个综合示例(如计算圆面积、购物打折、变量交换及银行利息计算)巩固所学知识。完成相关作业将进一步加深对这些基础概念的理解与实践能力。
217 13
|
9月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
349 0
|
5月前
|
小程序 Java 知识图谱
Java 学习笔记 —— BMI & BMR 计算器
这是一个使用 Java 编写的 BMI 与 BMR 计算器小程序,可输入年龄、性别、身高和体重,计算身体质量指数(BMI)和基础代谢率(BMR),并输出健康评估结果。通过该项目,掌握了 Java 的输入处理、数据验证、条件判断、数学运算及格式化输出等基础知识,是 Java 初学者的理想练习项目。