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

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

  • 原子性:一个线程在执行的过程当中不被中断。
  • 可见性:一个线程修改了共享变量,另一个线程能够马上看到,就叫做可见性。
  • 有序性:编译指令重排导致的问题。
相关文章
|
2月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
66 2
|
2月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
65 1
|
8天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
21 1
|
1月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
2月前
|
存储 Java
Java 中 ConcurrentHashMap 的并发级别
【8月更文挑战第22天】
48 5
|
2月前
|
存储 算法 Java
Java 中的同步集合和并发集合
【8月更文挑战第22天】
38 5
|
2月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
49 2
|
2月前
|
Java 开发者
【编程高手必备】Java多线程编程实战揭秘:解锁高效并发的秘密武器!
【8月更文挑战第22天】Java多线程编程是提升软件性能的关键技术,可通过继承`Thread`类或实现`Runnable`接口创建线程。为确保数据一致性,可采用`synchronized`关键字或`ReentrantLock`进行线程同步。此外,利用`wait()`和`notify()`方法实现线程间通信。预防死锁策略包括避免嵌套锁定、固定锁顺序及设置获取锁的超时。掌握这些技巧能有效增强程序的并发处理能力。
25 2
|
3月前
|
Java 开发者
Java中的多线程与并发控制
【7月更文挑战第31天】在Java的世界中,多线程是提升程序性能和响应能力的关键。本文将通过实际案例,深入探讨Java多线程的创建、同步机制以及并发包的使用,旨在帮助读者理解并掌握如何在Java中高效地实现多线程编程。
46 3
|
3月前
|
负载均衡 NoSQL Java