【并发编程的艺术】JAVA 原子操作实现原理

简介: 本篇把原子操作单独拿出来详细阐述,结合前面两篇文章中的CPU多级缓存结构进行串联,加深理解。下一篇会全面研究Java的内存模型。

系列文章:

【并发编程的艺术】JVM 体系与内存模型

【并发编程的艺术】JAVA 并发机制的底层原理

前一章中,我们描述了volatile 和 synchronized的实现原理,本篇介绍原子操作特性、

一 相关定义

1.1 缓存行(Cache line)

通过前面学习,已经知道cpu的多级缓存结构,缓存行是缓存的最小操作单位。

1.2 CAS

即Compare And Swap,比较并交换。java的unsafe包中提供了相关方法。

1.3 CPU流水线

CPI pipeline,工作方式类似工业生产上的装配流水线,CPU中多个不同功能的单元组成一条指令流水线。一条指令可能会被分为5-6个步骤之后再分别执行,来实现一个CPU时钟周期内完成一条指令,提高运算速度。

1.4 内存顺序冲突

Memory order violation。一般是由“假共享”引起。假共享,是指多CPU同时修改一个缓存行的不同部分,导致其中一个CPU的操作无效。出现内存顺序冲突时,CPU必须清空流水线。

二 操作系统中原子操作的实现原理

CPU提供两个机制来保证复杂内存操作的原子性:总线锁定 和 缓存锁定。

2.1 总线锁

先研究一个典型案例:i++。当两个CPU中同时对共享变量i执行++操作时,我们预期结果是i=3,但实际的结果可能是2:

为什么会出现这样的情况?(回想上一篇文章中提到过的cpu缓存结构),各CPU可能同时从自己的缓存中读取变量i的值,分别进行加1操作,再分别写入系统内存。要保证i++底层操作的原子性,就必须保证CPU1修改共享变量时,CPU2不能同时执行修改操作。

所谓总线锁,就是CPU提供LOCK#信号,当某个CPU在总线上输出这个信号时,其他CPU请求会被阻塞。

关于总线,再回顾一下CPU多级缓存结构,下图中的Bus就是总线:

另外,这个LOCK信号是否有点眼熟?这就是前篇中的:

2.2 缓存锁

总线锁是通过锁住总线来确保某个CPU可以独占共享内存,这样锁的开销其实是比较大的。这会导致锁定期间其他CPU无法操作其他内存地址的数据。其他CPU其实也可以考虑,只需要保证某个时刻对一个固定的内存地址的操作是原子性的即可,这样就降低了锁的粒度。

频繁使用的内存(数据)会缓存在CPU的L1、L2,L3级缓存,所以原子操作可以在各级缓存中进行。

缓存锁定,是指内存区域如果被缓存在CPU的缓存行中,并且在Lock操作期间被锁定,那么当执行锁操作回写内存时,CPU不能在总线上声言LOCK#新号,而是修改内存的内存地址,并通过缓存一致性机制阻止同时修改由两个及两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行的数据时,使缓存行无效。

2.3 缓存行不会被使用的场景

1、操作的数据不能被缓存在CPU的缓存内,或操作的数据跨多个缓存行时,CPU会调用总线锁定;

2、有些CPU不支持缓存锁定。已知这类的CPU:Intel 486和Pentium,就算锁定的内存区域在处理器的缓存行中,也会调用总线锁定。

三 Java中的原子操作实现

两种方式:锁机制 和 CAS。

3.1 锁机制

锁机制,要求只有获得了锁的线程才能操作锁定的内存区域。JVM中的锁包括偏向锁、轻量级锁和重量级锁(互斥锁)。从具体类或命令的角度说,有最常用的synchronized 和 Lock。

3.2 CAS

JVM中的CAS是利用CPU提供的CMPXCHG指令来实现的,通常会配合“自旋”使用,即循环进行CAS直到成功为止。

jdk1.5之后,在并发包中提供了诸如AtomickInteger、AtomicLong等方法来执行对应类型变量的原子更新。

CAS存在的问题:

1、ABA

这也是了解过CAS都知道的典型问题。“比较”,是检查值是否发生了变化,但当一个变量原来的值是A,先变成B后又变成A,这种情况CAS的检查机制会误判为没有发生变化。

解决思路:使用版本号。每次更新时把版本号加1,A->B->A就变成 1A->2B->3A。jdk1.5及以后,Atomic包中提供了类AtomicStampedReference来解决ABA。结构如下:

/**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

V expectedReference => 预期引用 ,

V newReference => 更新后的引用,

int expectedStamp => 预期标志,

int newStamp => 更新后的标志。

2、循环时间长开销大

循环是为了保证占用CPU,从而避免上下文切换带来的时间消耗。但这会导致一直占用CPU资源。

3、只保证一个共享变量的原子操作

这点是比较好理解的,因为比较、替换都是针对一个变量的操作,所以不适用多个共享变量同时操作的场景。这种情况通常还是会通过锁来实现;或者也可以取巧把多个共享变量合成一个共享变量来操作。

四 总结

本篇把原子操作单独拿出来详细阐述,结合前面两篇文章中的CPU多级缓存结构进行串联,加深理解。下一篇会全面研究Java的内存模型。

相关文章
|
3月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
1月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
34 0
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
193 6
|
2月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
51 2
|
2月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
118 0
|
4月前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
4月前
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
38 5
|
Java
Java面试题 synchronized底层实现原理?它与lock相比有什么优缺点?
Java面试题 synchronized底层实现原理?它与lock相比有什么优缺点?
269 0
|
5天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
44 17