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 并没有初始化。

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

  • 原子性:一个线程在执行的过程当中不被中断。
  • 可见性:一个线程修改了共享变量,另一个线程能够马上看到,就叫做可见性。
  • 有序性:编译指令重排导致的问题。
相关文章
|
18天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
2月前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
66 2
|
2月前
|
Java 数据库连接 API
Spring 框架的介绍(Java EE 学习笔记02)
Spring是一个由Rod Johnson开发的轻量级Java SE/EE一站式开源框架,旨在解决Java EE应用中的多种问题。它采用非侵入式设计,通过IoC和AOP技术简化了Java应用的开发流程,降低了组件间的耦合度,支持事务管理和多种框架的无缝集成,极大提升了开发效率和代码质量。Spring 5引入了响应式编程等新特性,进一步增强了框架的功能性和灵活性。
59 0
|
3月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
41 1
|
4月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(基础篇)
从Java环境的搭建到实际代码的编写,从基本用法的讲解到底层原理的剖析,深度解析Java基础知识。本文是《Java学习路线》专栏的起始文章,旨在提供一套完整的Java学习路线,覆盖Java基础知识、数据库、SSM/SpringBoot等框架、Redis/MQ等中间件、设计模式、架构设计、性能调优、源码解读、核心面试题等全面的知识点,并在未来不断更新和完善,帮助Java从业者在更短的时间内成长为高级开发。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(基础篇)
|
4月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
本文是Java基础的进阶篇,对异常、集合、泛型、Java8新特性、I/O流等知识进行深入浅出的介绍,并附有对应的代码示例,重要的地方带有对性能、底层原理、源码的剖析。适合Java初学者。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
|
4月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
3月前
|
Java 数据安全/隐私保护
java学习笔记(基础习题)
java学习笔记(基础习题)
53 0