Java中原子操作的实现原理
创始人
2025-05-30 09:34:46
0

Java中原子操作的实现原理

    • 1. 什么是原子操作
    • 2. 处理器如何是实现操作的原子性
      • 2.1 使用总线锁保证原子性
      • 2. 2 使用缓存锁保证原子性
    • 3. Java如何实现原子操作
      • 3.1 使用循环CAS实现原子操作
      • 3.2 CAS实现原子操作的三大问题
        • 3.2.1 ABA问题
        • 3.2.2 循环开销时间大问题
        • 3.2.3 只能保证一个共享变量的原子操作
      • 3.2 使用锁机制实现原子操作

1. 什么是原子操作

我们在学习MySQL的时候就了解过原子性,即整个事务是不可分割的最小单位,事务中任何一个语句执行失败,所有已经执行成功的语句也要回滚,整个数据库状态要恢复到执行事务前到状态。Java中的原子性其实跟先前在数据库的内容里面说的类似,就是不可在分割,在我们的多线程里面就是相当于一把锁,在当前的线程没有完成对应的操作之前,别的线程不允许切换过来,那么Java中是如何实现代码操作中的原子性的呢?在说明这个问题之前,我们先来看一些术语,方便接下来的理解。

在这里插入图片描述

2. 处理器如何是实现操作的原子性

处理器通常采用缓存加锁或者总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从内存中读取或者写入一个字节是原子的,意思是,当一个处理器读取一个字节时,其他处理器就不能访问这个字节的内存地址。Pentium 6和最新的处理器可以保证单处理器对于同一个缓存行进行的16/32/64位的操作是原子性的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存行锁定两个操作来保证复杂内存操作的原子性。

2.1 使用总线锁保证原子性

第一个机制就是通过总线锁保证原子性。如果多个处理器同时对共享变量进行改写(例如i++),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值就会个期望的值不一致。
在这里插入图片描述
原因可能是读个处理器同时从各自的缓存中读取数变量i,分别进行加1操作,然后分别写入各自的系统内存中。那么要想保证读改写操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
这里顺便说一下JMM也就是Java的内存模型。
在这里插入图片描述

上图是传统的计算机架构,组成包括以下几个
(1)CPU

买过服务器的先伙伴都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个核,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。

(2)CPU Register

CPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。

(3)CPU Cache Memory

CPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。

(4)Main Memory

Main Memory 就是主存。

主存比 L1、L2 缓存要大很多,部分高端机器还有 L3、L4 缓存。

2. 2 使用缓存锁保证原子性

第二个机制就是使用缓存锁来保证原子性。在同一时刻,我们只需要对某个内存地址的操作是原子性的即可,但总线锁吧CPU和内存之间的通信锁住了,这使得锁顶期间,其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大,目前处理器在某些场合下适应缓存锁来代替总线锁进行优化。

频繁使用的内存会缓存在L1、L2、L3高速缓存中,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在Pentium 6和目前的处理器中,可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当他执行锁操作回写到内内存时,处理器不在总线上声明LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存数据区域数据,当其他处理器回写已被修改的缓存行的数据时,会使缓存行无效。

但是有两种情况处理器不会使用缓存锁定

  • 第一种情况就是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定。
  • 第二种情况是:有些处理器不支持缓存锁定。

3. Java如何实现原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作。

3.1 使用循环CAS实现原子操作

JVM中的CAS操作利用的正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS直到成功。举例:

private AtomicInteger atomicI = new AtomicInteger(0);private int i = 0;public static void main(String[] args) {final Counter cas = new Counter();List ts = new ArrayList(600);long start = System.currentTimeMillis();for (int j = 0; j < 100; j++) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {cas.count();cas.safeCount();}}});ts.add(t);}for (Thread t : ts) {t.start();}// 等待所有线程执行完成for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(cas.i);System.out.println(cas.atomicI.get());System.out.println(System.currentTimeMillis() - start);}/*** 使用 CAS 实现线程安全计数器*/private void safeCount() {for (; ; ) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;}}}/*** 非线程安全计数器*/private void count() {i++;}

3.2 CAS实现原子操作的三大问题

在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的Xfer方法。虽然CAS很高效的解决了原子操作,但是CAS仍然存在三大问题:

1.ABA问题
2.循环时间长,开销大问题
3.只能保证一个共享变量的原子操作

3.2.1 ABA问题

因为CAS需要在操作值得时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它得值没有发生变化,但实际上却发生变化了。ABA问题得解决思路就是使用版本号,每次变量更新的时候把版本号+1,那么A-B-A就会变成1A-2B-3A。从jdk1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志位的值设定为给定的更新值。

3.2.2 循环开销时间大问题

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率就会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟时间取决于具体的实现版本,在一些处理器上延迟时间为0;第二,它可以避免在退出循环的时候因为内存顺序冲突而引起CPU流水线被清空,从而提升CPU执行效率。

3.2.3 只能保证一个共享变量的原子操作

对一个共享变量进行CAS操作的时,我们可以使用循环CAS的方式来保证操作的原子性,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,还有一个取巧的方法就是把多个共享变量合并成一个共享变量来进行操作。从 Java 1.5 开始, JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。

3.2 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是,除了偏向锁,JVM实现锁的方式都使用了循环CAS,即当一个线程想要进入同步块时使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

相关内容

热门资讯

[Java Web]JSP基础... ⭐作者介绍:大二本科网络工程专业在读,持续学习Java,努...
044、复习day02 文章目录minds一、面经01、HTTP02、TCP03、JUC04、SE基础二、力扣01、对称的二...
CentOS下安装Redis7... Redis安装配置 一、环境准备 操作系统:CentOS 7 如何确定linux是多...
八年级数学教学工作计划 八年级数学教学工作计划(15篇)  日子在弹指一挥间就毫无声息的流逝,迎接我们的将是新的生活,新的挑...
物业工作总结及工作计划 物业工作总结及工作计划15篇  总结是指社会团体、企业单位和个人在自身的某一时期、某一项目或某些工作...
学生会文艺部工作计划 学生会文艺部工作计划  又是一个新的学期,新的开始,我们迎来了新的挑战和新的机遇;对于文艺部的组建,...
python正则化 一、re模块简介聊到Python正则表达式的支持,首先肯定会想到re库,...
高二下学期班主任工作计划范文 高二下学期班主任工作计划范文  高二下学期班主任工作计划范文(精选30篇)  时间过得真快,总在不经...
保密检查专项工作计划 保密检查专项工作计划范本(精选6篇)  计划的重要性渗透于整个组织经营的各个方面,贯穿于经营的全过程...
渗透学习-CTF篇-web-C... 文章目录前言web入门部分反序列化web254web255web256web257web258 前...
面向状态机编程:复杂业务逻辑应... 一、背景在研发项目中,经常能遇到复杂的状态流转类的业务场景,比如游戏编程...
通信网络智能运维 移动网络的智能运维是一个日益重要的话题,因为随着移动设备的普及和移动应用程序的增加&#...
leetcode-链表-2.两... 2. 两数相加 - 力扣(Leetcode)   class Solu...
中职班主任工作计划 【推荐】中职班主任工作计划3篇  时光在流逝,从不停歇,我们的工作又进入新的阶段,为了在工作中有更好...
自然社团的工作计划 自然社团的工作计划  一、工作目的  使自然社在新的一学年有新的发展、新的提高,同时也为新社员提供良...
mysql8下载安装(zip版... mysql官网下载最新安装包 官网地址:https://dev.mysql.com/d...
德育工作计划 关于德育工作计划模板集合9篇  日子在弹指一挥间就毫无声息的流逝,又将迎来新的工作,新的挑战,不妨坐...
小学班级安全教育工作计划   安全工作关系到师生生命的安危,是学校开展正常教育、教学工作和社会稳定的头等大事,下面是CN人才网...
中班年级组工作计划 中班年级组工作计划(通用6篇)  光阴如水,又将迎来新的工作,新的挑战,一定有不少可以计划的东西吧。...
屏幕后处理 高斯模糊和Bloo... 高斯模糊 滤波:在图像处理中,通过滤波强调图片的一些特征或去除图片中一些...