对于i++的原子性问题的详析

简介: 对于i++的原子性问题的详析

i++不是一个原子性操作,首先作为结论摆出这句话。

那么什么是原子性操作?简单说就是该操作是不可分割的,或者说编译后的指令就是一句话。

既然说i++不是原子性操作,那么是几步呢?是三步,那么如何证明呢?

写一个最简单的代码来看看:

public class Test
{
    public int i = 10;
    public void increase(){
        i++;
    }
}

命令行中javac命令编译生成class文件,javap命令查看class文件的反汇编结果。

结果如下:

public class Test {
  public int i;
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field i:I
      10: return
  public void increase();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field i:I
      10: return
}

Test()的构造方法我们不管,看到下面的increase方法中的内容,嗯,看不懂,解释一下:


aload_0 将this引用推送至栈顶。

dup 复制栈顶值this应用,并将其压入栈顶,即此时操作数栈上有连续相同的this引用。

getfield 弹出栈顶的对象引用,获取其字段race的值并压入栈顶。第一次操作。

iconst_1 将int型(1)推送至栈顶

iadd 弹出栈顶两个元素相加(race+1),并将计算结果压入栈顶。第二次操作。

putfield 从栈顶弹出两个变量(累加值,this引用),将值赋值到this实例字段race上。第三次操作,赋值。


这是汇编层面的对于i++的原子性的理解。那么代码层面呢?

public class Test
{
    public int race = 0;
    public void increase() {
        race++;
    }
    public static void main(String[] args) {
        //创建5个线程,同时对同一个volatileTest实例对象执行累加操作
        Test test=new Test();
        int threadCount = 10;
        Thread[] threads = new Thread[threadCount];//5个线程
        for (int i = 0; i < 10; i++) {
            //每个线程都执行1000次++操作
            threads[i]  = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            });
            threads[i].start();
        }
        //等待所有累加线程都结束
        for (int i = 0; i < threadCount; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("累加结果:"+test.race);
    }
}

控制台输出结果是:累加结果:9651(并无法保证每一次执行后,累加结果都是小于10000的,多执行几次肯定会有)

我们可以看到,按理说race这个变量自增了10000次,最终累加结果应该是10000,但是实际结果却是小于10000的。这就是race++这行代码带来的结果。


那么如何避免因为race++不保证原子性带来的问题呢?

一个是可以在increase方法上添加synchronized关键字。来保证race++的操作是具有原子性的。

public synchronized void increase() {
     race++;
}

或者使用AtomicInteger直接进行原子操作来实现也可以:

AtomicInteger atomicInteger = new AtomicInteger(0);
public void addAtomic() {
    atomicInteger.getAndIncrement();
}

顺便说另一个小问题:

在i++的原子性问题的代码中,在输出累加结果之前,判断前面的10个线程是否都已经执行完的,这里使用的是遍历线程,然后调用join方法的。

还有一种写法是:

while(Thread.activeCount()>1)
 {
       Thread.yield();
 }

注意,这里的判断条件与java 的IDE有关,如果在eclipse中的话,while条件中是Thread.activeCount()>1是没有问题的。

但是如果是使用IntelliJ IDEA的话,如果还是Thread.activeCount()>1的话,那么会一直在while循环中无法跳出,要使用Thread.activeCount()>2的话才可以。

使用代码来验证:

public class TheadNameTest {
    public static void main(String[] args) {
        System.out.println(findAllThreads());
        System.out.println(Thread.activeCount());
    }
    public static String findAllThreads() {
        ThreadGroup group =
                Thread.currentThread().getThreadGroup();
        ThreadGroup topGroup = group;
        // 遍历线程组树,获取根线程组
        while ( group != null ) {
            topGroup = group;
            group = group.getParent();
        }
        // 激活的线程数加倍
        int estimatedSize = topGroup.activeCount() * 2;
        Thread[] slackList = new Thread[estimatedSize];
        //获取根线程组的所有线程
        int actualSize = topGroup.enumerate(slackList);
        // copy into a list that is the exact size
        Thread[] list = new Thread[actualSize];
        System.arraycopy(slackList, 0, list, 0, actualSize);
        String threadName = "";
        for (Thread thread : list) {
            threadName += thread.getName()+"#";
        }
        return threadName;
    }
}}

在IDEA中使用run执行的话,结果为:

Reference Handler#Finalizer#Signal Dispatcher#Attach Listener#main#Monitor Ctrl-Break#
2

在IDEA中使用debug,

或eclipse中使用run,

或eclipse中使用debug,

或使用java的命令行执行的话,结果都为

Reference Handler#Finalizer#Signal Dispatcher#Attach Listener#main#
1

明显看到所有线程的名称中多了一个Monitor Ctrl-Break,而且activeCount活跃线程也是多了一个。这就是为什么在IDEA中要Thread.activeCount()>2的原因。

那么这个Monitor Ctrl-Break线程是干什么的呢?又是怎么出现的呢?

目录
相关文章
|
Java
Java 清空 List 的多种方法?
Java 清空 List 的多种方法?
2745 0
|
JSON 前端开发 JavaScript
3分钟让你学会axios在vue项目中的基本用法(建议收藏)
3分钟让你学会axios在vue项目中的基本用法(建议收藏)
658 0
|
对象存储 开发者
对象OSS生命周期(LifeCycle)管理功能|学习笔记
快速学习对象 OSS 生命周期(LifeCycle)管理功能
3073 0
对象OSS生命周期(LifeCycle)管理功能|学习笔记
|
缓存 负载均衡 算法
“软件系统三高问题”高并发、高性能、高可用系统设计经验
​ 总的来说解决三高问题核心就是 “分字诀” 业务分层、系统分级、服务分布、数据库分库/表、动静分离、同步拆分成异步、单线程分解成多线程、原数据缓存分离、分流等等。。。。 直观的表述就是:从前端用的CDN、动静分离,到后台服务拆分成微服务、分布式、负载均衡、缓存、池化、多线程、IO、分库表、搜索引擎等等。都是强调一个“分”字。
4066 0
“软件系统三高问题”高并发、高性能、高可用系统设计经验
|
11月前
|
机器学习/深度学习 人工智能 算法
AI在医疗健康领域的应用
随着人工智能技术的不断发展,其在医疗健康领域的应用也日益广泛。从辅助诊断、个性化治疗方案的制定,到疾病预防和健康管理,AI技术都在发挥着重要作用。本文将探讨AI在医疗健康领域的应用,包括其在医学影像分析、基因编辑、药物研发等方面的应用,以及其对医疗行业未来发展的影响。
|
11月前
|
机器学习/深度学习 人工智能 测试技术
VisionTS:基于时间序列的图形构建高性能时间序列预测模型,利用图像信息进行时间序列预测
构建预训练时间序列模型的主要挑战在于获取高质量、多样化的时间序列数据。目前有两种方法:迁移学习LLM(如GPT-4或Llama)和从零训练。尽管迁移学习可行,但效果有限;从零训练则依赖大量数据,如MOIRAI、TimesFM和TTM等模型所示。为解决这一难题,研究人员提出利用图像数据进行时间序列预测。
715 11
VisionTS:基于时间序列的图形构建高性能时间序列预测模型,利用图像信息进行时间序列预测
|
11月前
|
Java Maven
Maven 依赖管理
Maven 一个核心的特性就是依赖管理。当我们处理多模块的项目(包含成百上千个模块或者子项目),模块间的依赖关系就变得非常复杂,管理也变得很困难。针对此种情形,Maven 提供了一种高度控制的方法。
400 5
|
供应链 Python
Demand Forecasting模型解释与Python代码示例
Demand Forecasting模型解释与Python代码示例
|
监控 Oracle 关系型数据库
关系型数据库Oracle恢复测试
【7月更文挑战第20天】
251 7
|
开发工具 git
服务器定时自动拉取Git仓库代码自动部署
服务器定时自动拉取Git仓库代码自动部署
490 0