什么是原子操作?Java如何实现原子操作?

news/2024/9/23 19:05:50

1.什么是原子操作?

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

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

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

2.1使用总线锁保证原子性:

如果多个处理器同时对共享变量进行改写(例如i++),那么共享变量就会被多个处理器同时进行操作,这样读写操作就不是原子的,操作完之后共享变量的值就会和期望值不一致。

原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入各自的内存中。那么要想保证读和写是原子性的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线索就是使用处理器提供一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器 的请求将被阻塞住,那么该处理器可以独占共享资源。

这里顺便说一下,JVM也就是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就是主存。

2.2使用缓存锁保证原子性:

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

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

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

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

3.Java如何实现原子操作?

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

3.1使用CAS实现原子操作

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

package com.cl.pattern.cas;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;/*** @desc author Chen lei* @date 2024/9/22 18:29*/
public class Counter {private AtomicInteger atomicInteger = new AtomicInteger(0);private int i = 0;public static void main(String[] args) {final Counter cas = new Counter();List<Thread> ts = new ArrayList<Thread>(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) {throw new RuntimeException(e);}}System.out.println(cas.i);System.out.println(cas.atomicInteger.get());System.out.println(System.currentTimeMillis() - start);}private void safeCount(){for (;;){int i = atomicInteger.get();boolean suc = atomicInteger.compareAndSet(i,++i);if (suc){break;}}}private void count(){i++;}
}

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

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

3.2.1ABA问题:

因为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释放锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/63879.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

JAVA基础之八-方法变量作用域和编译器

本文主要讨论方法中变量作用域。不涉及类属性变量、静态变量、线程变量共享等。 虽然知道某类变量的作用域非常重要,但是没有太多需要说的,因为许多东西是显而易见,不言自明。在大部分情况下,或者在老一点版本中,java语法看起来都比较正常,或者说相对古典。 但是随着JAVA…

信息学奥赛复赛复习01-CSP-J2019-01-字符、字符数组、字符串、string、字符串读取

信息学奥赛复赛复习01-CSP-J2019-01-字符、字符数组、字符串、string、字符串读取 PDF文档公众号回复关键字:202409231 2019 CSP-J 题目1 数字游戏 [题目描述] 小 K 同学向小 P 同学发送了一个长度为 8 的 01 字符串来玩数字游戏,小 P 同学想要知道字符串中究竟有多少个 1。 注…

学习高校课程-软件工程-理解需求(ch8)

REQUIREMENTS ENGINEERING 需求工程 Requirements engineering encompasses seven distinct tasks: inception, elicitation,elaboration, negotiation, specification, validation, and management Inception 启动 At project inception, you establish a basic understanding…

局域网远程命令重启电脑

只要知道远程服务器的管理员密码和IP地址,在局域网中的任意一台机器上打开“命令提示符”窗口,运行以下命令:1、获取远程服务器的管理员权限net use IP地址 "管理员密码" /user:administrator2、使用shutdown命令远程重启服务器shutdown /r /t 0 /m IP地址这样的…

Hexo-GitHub部署魔改第一步-config

Hexo-GitHub部署魔改第一步_config.yml 1. config.yml # Hexo Configuration ## Docs: https://hexo.io/docs/configuration.html ## Source: https://github.com/hexojs/hexo/# Site # 设置博客的标题 title: Your Blog Title # 子标题,可选 subtitle: xxxxx # 博客的描述,可…

git credential

远程访问github仓库时,git credential可以帮助我们避免重复输入用户密码并提高安全性。但是在本地计算机切换github用户后,如果不更新git credential,将会导致没有权限访问私有仓库或者push共有仓库。 对于 Windows 用户,打开 控制面板 -> 凭据管理器,找到与 GitHub 相…

高级语言程序设计第1次作业

班级链接:https://edu.cnblogs.com/campus/fzu 作业要求链接:https://edu.cnblogs.com/campus/fzu/2024C/homework/13264 学号:102400126 姓名:苏钦晨2.1 这个在课堂上完成任务后,理解了各个位置的含义,并举一反三,尝试去删去一些字符,仍可以继续运行,但不知道这些字符…

java如何调用外部程序

java如何调用外部程序 2017-03-15 20:50 179人阅读 评论(0) 收藏 举报 分类:Java应用(26) 版权声明:本文为博主原创文章,未经博主允许不得转载。引言;有时候有些项目需求,直接使用Java编写比较麻烦,所有我们可能使用其他语言编写的程序来实现。那么我们如何在java中调…