0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

在多线程的情况下如何对一个值进行 a++ 操作

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-10-13 11:17 次阅读

在多线程的情况下,对一个值进行 a++ 操作,会出现什么问题?

a++ 的问题

先写个 demo 的例子。把 a++ 放入多线程中运行一下。定义 10 个线程,每个线程里面都调用 5 次 a++,把 a 用 volatile 修饰,可以让 a 的值在修改之后,所有的线程立刻就可以知道。最后结果是不是 50,还是其他的数字?

public class Test {

    private static volatile  int a = 0;

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable(){

                @Override
                public void run() {
                   try {
                        for(int j = 0; j < 10; j++) {
                            System.out.print(a++ + ", ");
                            Thread.sleep(100);
                        }
                    } catch (Exception e) {

                    }
                }
            });
            threads[i].start();
        }
    }
}

图片

从结果上看 a++ 的操作并没有达到预期值的 50,而是少了很多,其中还有一定是有问题的。那就是因为 a++ 的操作并不是原子性的。

原子性

并发编程,有三大原则:有序性、可见性、原子性

  1. 有序性:正常编译器执行代码,是按顺序执行的。有时候,在代码顺序对程序的结果没有影响时,编译器可能会为了性能从而改变代码的顺序。
  2. 可见性:一个线程修改了一个变量的值,另外一个线程立刻可以知道修改后的值。
  3. 原子性:一个操作或者多个操作在执行的时候,要么全部被执行,要么全部都不执行。

上面的 a++ 就没有原子性,它有三个步骤:

  1. 在内存中读取了 a 的值。
  2. 对 a 进行了 + 1 操作。
  3. 将新的 a 值刷回到内存。

这三个步骤可以被示例中的 10 个线程上下文切换打断:当 a = 10

  1. 线程 1 将 a 的值读取到内存, a = 10
  2. 线程 2 将 a 的值读取到内存, a = 10
  3. 线程 1 将 a + 1,a = 11
  4. 此时线程发生切换,线程 2 对 a 进行 + 1 操作, a = 11
  5. 线程 2 将 a 的值写回到内存, a = 11
  6. 线程 1 将 a 的值写回到内存, a = 11

从上面的步骤中可以看出 a 的值在两次相加后没有得到 12 的值,而是 11。这就是 a++ 引发的问题。

小 B 把上面的步骤对面试官讲了一遍,面试官又问了,有什么方式可以避免这个问题,小 B 不加思索的回答用 synchronized 加锁。面试官说 synchronized 太重了,还有其他的解决方式吗?小 B 晕了。其实可以使用 AtomicInteger 的 incrementAndGet() 方法。

AtomicInteger 源码分析

主要属性

首先看看 AtomicInteger 的主要属性。

//sun.misc 下的类,提供了一些底层的方法,用于和操作系统交互
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 字段的内存地址相对于对象内存地址的偏移量
private static final long valueOffset;
//通过 unsafe 初始化 valueOffset,获取偏移量
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

// 用 valatile 修饰的值,保证了内存的可见性
private volatile int value;

从属性中可以看出 AtomicInteger 调用的是 Unsafe 类,Unsafe 类中大多数的方法是用 native 修饰的,可以直接进行一些系统级别的操作。

用 volatile 修饰 value 值,保证了一个线程的值对另外一个线程立即可见。

incrementAndGet()

//AtomicInteger.incrementAndGet()
public final int incrementAndGet() {
    //调用 unsafe.getAndAddInt()
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//Unsafe.getAndAddInt()
//参数:需要操作的对象,偏移量,要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

//Unsafe.compareAndSwapInt()
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

incrementAndGet() 首先获取了当前值,然后调用 compareAndSwapInt() 方法更新数据。

compareAndSwapInt() 是 CAS 的缩写来源,比较并替换。被 native 修饰,调用了操作系统底层的方法,保证了硬件级别的原子性。

var2,var4,var5 是它的三个操作数,表示内存地址偏移量 valueOffset,预期原值 expect,新的值 update。把 this.compareAndSwapInt(var1, var2, var5, var5 + var4) 变成 this.compareAndSwapInt(obj, valueOffset, expect, update),释义就是如果内存位置中的 valueOffset 值 与 expect 的值相同,就把内存中的 valueOffset 改成 update,否则不操作。

getAndAddInt() 方法中用了 do-while,就相当于如果 CAS 一直更新不成功,就不退出循环。直到更新成功为止。

ABA 问题

CAS 操作也并不是没有问题的。

  1. 循环操作时间长了,开销大。用了 do-while,如果更新一直不成功,就一直在循环。会给 CPU 带来很大的开销。
  2. 只能保证一个共享变量的原子性。循环 CAS 的方式只能保证一个变量进行原子操作,在对多个变量进行 CAS 的时候就没办法保证原子性了。
  3. ABA 问题。CAS 的操作一般是 1. 读取内存偏移量 valueOffset。2. 比较 valueOffset 和 expect 的值。3. 更新 valueOffset 的值。如果线程 A 读取 valueOffset 后,线程 B 修改了 valueOffset 的值,并且将 valueOffset 的值又改了回来。线程 A 会认为 valueOffset 的值并没有改变。这就是 ABA 问题。要解决这个问题,就是在每次修改 valueOffset 值的时候带上一个版本号。

总结

这篇文章介绍了 CAS,它是 java 中的乐观锁,每次认为操作并不会有其他线程去修改数据,如果有其他线程操作了数据,就重试,一直到成功为止。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编程
    +关注

    关注

    88

    文章

    3615

    浏览量

    93716
  • 多线程
    +关注

    关注

    0

    文章

    278

    浏览量

    19951
  • 代码
    +关注

    关注

    30

    文章

    4787

    浏览量

    68589
收藏 人收藏

    评论

    相关推荐

    Java多线程的用法

    本文将介绍一下Java多线程的用法。 基础介绍 什么是多线程 指的是进程中同时运行多个
    的头像 发表于 09-30 17:07 949次阅读

    多线程编程之: 问题提出

    多线程编程之 问题提出编写耗时的单线程程序:  新建
    发表于 10-22 11:41

    LabView的多线程语言

    LabView的多线程语言以前只会照猫画虎的写些简单的程序,些基本原理不是很清晰。从网上找了些资料,这里总结一下。1。
    发表于 06-08 10:13

    基于51单片机的多线程操作系统 精选资料分享

    我知道,51单片机上运行操作系统,大多数情况下并不实用。但51单片机广为人知。所以我认为,用它来逐步的实现
    发表于 07-20 07:55

    如何使用多线程和异步操作等并发设计方法来最大化程序的性能

      因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可
    发表于 08-23 16:31

    MCU开发中使用多线程操作读是否需要保护?

    ,那么多线程访问是安全的,那么对于读,某些情况下需要保护,某些情况下其实可以不需要保护。
    发表于 02-01 15:42

    很多变量多线程读写是使用关中断好还是使用互斥进行保护呢?

    我想问一下,就是我有很多变量会多线程读写操作,有些会比较频繁,我读写的时候是使用中断去保护还是增加互斥量去保护。 1.如果加互斥量,当前低优先级读写
    发表于 05-05 14:14

    QNX环境多线程编程

    介绍了QNX 实时操作系统和多线程编程技术,包括线程间同步的方法、多线程程序的分析步骤、线程基本程序结构以及实用编译方法。QNX 是由加拿大
    发表于 08-12 17:37 30次下载

    基于多线程环境的递增操作--原子操作

    因此多线程环境中对变量进行读写时,我们需要有种方法能够保证对
    的头像 发表于 01-10 11:16 6178次阅读
    基于<b class='flag-5'>多线程</b>环境<b class='flag-5'>下</b><b class='flag-5'>值</b>的递增<b class='flag-5'>操作</b>--原子<b class='flag-5'>操作</b>

    Linux多线程编程

    线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。  使用多线程的理由之是和进程相比,它是
    发表于 04-02 14:43 606次阅读

    怎样才能在不加锁的情况下解决多线程问题

    我们知道,多线程同时修改共享变量时会出现数据不致的问题,比如多个线程同时对变量加1,假设count的初始
    的头像 发表于 03-02 09:31 476次阅读
    怎样才能在不加锁的<b class='flag-5'>情况下</b>解决<b class='flag-5'>多线程</b>问题

    基于QT自制上位机(多线程

    前言:应用程序某些情况下需要处理比较复杂的逻辑,例如常规的图传上位机,如果在传输图片跑到较高码流或对图像执行些处理任务是,引用多线程可以明显 改善响应度和反馈速度。 QT
    发表于 05-09 11:47 1次下载
    基于QT自制上位机(<b class='flag-5'>多线程</b>)

    多线程事务怎么回滚?简单示例演示多线程事务

    spring中可以使用@Transactional注解去控制事务,使出现异常时会进行回滚,多线程中,这个注解则不会生效,如果主线程需要先
    发表于 08-09 12:22 669次阅读
    <b class='flag-5'>多线程</b>事务怎么回滚?<b class='flag-5'>一</b><b class='flag-5'>个</b>简单示例演示<b class='flag-5'>多线程</b>事务

    什么情况下避免使用系统调用

    linux多线程环境对同变量进行读写时,经常会遇到读写的原子性问题,即会出现竞争条件。为了解决多个
    的头像 发表于 11-13 10:32 443次阅读
    什么<b class='flag-5'>情况下</b>避免使用系统调用

    c语言中a++是什么意思

    C语言中,a++自增运算符,用于对a进行
    的头像 发表于 11-26 09:19 1.8w次阅读