博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java中的原子操作
阅读量:3752 次
发布时间:2019-05-22

本文共 4359 字,大约阅读时间需要 14 分钟。

文章目录

1、什么是原子操作

原子操作:一个或多个操作在CPU执行过程中不被中断的特性

当我们说原子操作时,需要分清楚针对的是CPU指令级别还是高级语言级别

比如:经典的银行转账场景,是语言级别的原子操作;

而当我们说volatile修饰的变量的复合操作,其原子性不能被保证,指的是CPU指令级别。
二者的本质是一致的。

“原子操作”的实质其实并不是指“不可分割”,这只是外在表现,本质在于多个资源之间有一致性的要求,操作的中间态对外不可见

比如:在32位机器上写64位的long变量有中间状态(只写了64位中的32位);银行转账操作中也有中间状态(A向B转账,A扣钱了,B还没来得及加钱)

2、Java中原子操作的实现方式

Java使用锁和自旋CAS实现原子操作

2.1 用CAS实现原子操作

2.1.2 使用CAS实现原子操作

public class Counter {
private final AtomicInteger atomicI = new AtomicInteger(0); private int i = 0; public static void main(String[] args) {
Counter counter = new Counter(); ArrayList
list = new ArrayList<>(1000); long start = System.currentTimeMillis(); IntStream.range(0, 100).forEach(u -> {
list.add(new Thread(() -> IntStream.range(0, 1000).forEach(v -> {
counter.safeCount(); counter.count(); }))); }); list.forEach(Thread::start); /* wait for all the threads to complete*/ list.forEach(u -> {
try {
u.join(); } catch (InterruptedException e) {
e.printStackTrace(); } }); System.out.println(counter.i); System.out.println(counter.atomicI.get()); System.out.println(System.currentTimeMillis() - start); } /* 使用CAS 来实现原子操作*/ public void safeCount() {
for (; ; ) {
int i = atomicI.get(); /*Atomically sets the value to the given updated value if the current value == the expected value.*/ /*Parameters: expect - the expected value update - the new value*/ /* 其实,假如使用 原子类来实现计数器,不需要直接用 cas 的API,原子类已经提供了现成的API了*/ boolean success = atomicI.compareAndSet(i, i + 1); if (success) {
break; } } } /* 使用 锁 来实现原子操作*/ public synchronized void safeCount1() {
i++; } /* 线程不安全的累加*/ public void count() {
i++; }}

并发包中提供了很多原子类来支持原子操作:

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference
  • LongAdder

2.1.3 CAS实现原子操作的问题

CAS是并发包的基石,但用CAS有三个问题:

  • 1)ABA问题

    根源:CAS的本质是对变量的current value,期望值expected value进行比较,二者相等时,再将 给定值given update value设为当前值。

    因此会存在一种场景,变量值原来是A,变成了B,又变成了A,使用CAS检查时会发现值并未变化,实际上是变化了。

    对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。

    ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。

    JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。

    public boolean compareAndSet(@Nullable V expectedReference,                         V newReference,                         int expectedStamp,                         int newStamp)Atomically sets the value of both the reference and stamp to the given update values if the current reference is == to the expected reference and the current stamp is equal to the expected stamp.Parameters:expectedReference - the expected value of the referencenewReference - the new value for the referenceexpectedStamp - the expected value of the stampnewStamp - the new value for the stamp
  • 2)循环时间长则开销大

    自旋CAS若长时间不成功,会对CPU造成较大开销。不过有的JVM可支持CPU的pause指令的话,效率可有一定提升。

    pause作用:

    • 延迟流水线指令(de-pipeline),使CPU不至于消耗过多执行资源。
    • 可避免退出循环时因内存顺序冲突(memorey order violation )引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  • 3)只能保证一个共享变量的原子操作

    CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。

    另外,也可“取巧”,将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab.

    JDK5提供AtomicReference保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。

3、Java中使用锁实现原子操作

锁机制保证只有拿到锁的线程才能操作锁定的内存区域。

JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。
synchronized锁定的临界区代码对共享变量的操作是原子操作。

4、CPU如何实现原子操作

首先,CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的,即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。

但对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定

  • 1、使用总线锁保证原子性

假如多个处理器同时读改写共享变量,这种操作(e.g. i++)不是原子的,操作完的共享变量的值会和期望的不一致。

原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。

总线锁就是解决此问题的。

总线锁:利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。

  • 2、使用缓存锁保证原子性

同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。

频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。

缓存锁是指:缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。

看下图:i 是同时被CPU1和CPU2缓存的内存区域变量;CPU1 修改缓存行中 i 时使用缓存锁定,则CPU2 不能同时缓存 i 的缓存行。(i 的缓存行会失效)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TMdvKCO2-1618653147136)(http://note.youdao.com/yws/res/36381/WEBRESOURCE8d20b5f4eb06205db479ed38b76d5592)]

转载地址:http://dknsn.baihongyu.com/

你可能感兴趣的文章
P1014 Cantor表
查看>>
实验十 算术编码
查看>>
实验二 二维随机变量信息量的计算
查看>>
使用react脚手架创建react项目时发生错误
查看>>
关于setState是异步与同步的
查看>>
56. 合并区间---js解法
查看>>
5. 最长回文子串---js解法
查看>>
USACO 2007 Open Gold/acwing2240:餐饮 (拆点+最大流)‘三分图匹配’
查看>>
那些年你不知道的C++STL进制转换函数
查看>>
区间和并问题 思路加模板整理(校门外的树)
查看>>
C++中next_permutation函数的使用方法、原理及手动实现
查看>>
网络流常用小技巧之 拆点
查看>>
最大权闭合子图
查看>>
最小权点覆盖集 与 最大权独立集
查看>>
POJ 2125 Destroying The Graph && Acwing 2325. 有向图破坏(拆点+最小权点覆盖集)
查看>>
计算几何基础知识整理大全 代码模板与证明过程 (直线、向量、多边形、三维计算几何、凸包、半平面交、最小圆覆盖)
查看>>
计算几何之 判断两线段是否相交 代码模板与证明
查看>>
三维计算几何之三维凸包 增量法
查看>>
MySQL变量,存储过程,函数,流程控制详解(小白都能懂哦)
查看>>
9篇小白都能懂系列博客学完MySQL基础
查看>>