首页 > 编程语言 >Java中的i++操作为什么不是线程安全的?

Java中的i++操作为什么不是线程安全的?

时间:2024-09-01 15:21:40浏览次数:5  
标签:count Java ++ int 计数器 线程 public

 

目录

1. 理解i++操作

2. 竞态条件的示例

3. 如何解决i++的线程安全问题?

需求场景:计数器的并发访问与统计

背景:

需求:

代码示例与问题分析:

预期结果:

实际结果:

解决方案一:使用 synchronized 关键字

解决方案二:使用 AtomicInteger

解决方案三:使用 ReentrantLock

需求模拟与扩展


        在Java中,i++ 是一个常见的操作符,用于将整数变量i的值增加1。虽然这个操作符看起来非常简单,但在多线程环境下,它却是线程不安全的。本文将详细探讨i++操作的线程安全问题,并提供相应的解决方案。

1. 理解i++操作

i++ 是一个复合操作,分为以下三个步骤:

  1. 读取当前值:从内存中读取变量i的当前值。
  2. 执行加法操作:对读取到的值执行加1操作。
  3. 写回结果:将计算后的结果写回变量i

在单线程环境中,这三个步骤是顺序执行的,因此不会出现问题。然而,在多线程环境下,多个线程可能会同时访问并修改同一个变量i,从而导致竞态条件

 

2. 竞态条件的示例

假设有两个线程(Thread A 和 Thread B),同时对变量i执行i++操作。以下是可能发生的情况:

  • i的初始值为5。
  • Thread A 读取i的值,得到5。
  • Thread B 也读取i的值,得到5。
  • Thread A 对i的值执行加1操作,结果为6,并将其写回变量i
  • Thread B 也对它读取到的i的值执行加1操作,结果为6,并将其写回变量i

最终,虽然进行了两次i++操作,i的值却仅增加了1次,最终结果仍为6。这种现象就是由于线程不安全导致的竞态条件。

 

3. 如何解决i++的线程安全问题?

需求场景:计数器的并发访问与统计

背景:

        假设你正在开发一个高并发的Web应用程序,该应用程序需要统计某个API接口的访问次数。这些访问次数将被存储在一个全局的计数器中,并且每次访问时计数器的值都会增加1。由于该API可能会被大量用户同时访问,因此需要考虑计数器的线程安全问题。

需求:
  1. 统计API访问次数:每次用户访问该API时,计数器增加1。
  2. 获取当前访问次数:能够安全地读取当前的访问次数。
  3. 支持高并发:系统需要支持大量的并发请求,并且保证统计的准确性。
代码示例与问题分析:

编写一个简单的计数器类,并模拟多个线程访问该计数器。

public class Counter {
    private int count = 0;

    // 增加计数器的值
    public void increment() {
        count++; 
    }

    // 获取当前计数器的值
    public int getCount() {
        return count; 
    } 
}

编写CounterTest测试类:模拟100个线程同时访问这个计数器

public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建100线程,每个线程调用1000次increment方法
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (int i = 0; i < 100; i++) {
            threads[i].join();
        }
        // 输出最终的计数器值
        System.out.println("Final count: " + counter.getCount());
    }
}
预期结果:

理论上,我们创建了100个线程,每个线程对计数器进行了1000次的increment操作,因此最终的计数器值应该为 100 * 1000 = 100000

实际结果:

运行代码后,你可能会发现计数器的值远小于1,000,00。这是因为i++不是线程安全的,导致多个线程可能在同一时刻读取和写入count变量,出现竞态条件。

 

解决方案一:使用 synchronized 关键字

可以通过在 increment() 方法上添加 synchronized 关键字来解决这个问题:

修改Counter类的代码:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

这样,每次只有一个线程能够执行 increment() 方法,从而避免了竞态条件。

 再次运行CounterTest类发现结果达到预期

解决方案二:使用 AtomicInteger

另一种方式是使用 AtomicInteger 类,该类提供了原子性的加法操作。

修改Counter类的代码:

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

AtomicInteger 能够确保 incrementAndGet() 操作是线程安全的,并且不需要使用锁,因此在高并发情况下性能更优。

juc并发编程指的就是这个包

再次运行CounterTest类发现结果达到预期

解决方案三:使用 ReentrantLock

如果需要更灵活的锁控制,可以使用 ReentrantLock

修改Counter类的代码:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

ReentrantLock 允许显式地控制锁的获取和释放,可以用在需要复杂同步的场景中。

 再次运行CounterTest类发现结果达到预期

需求模拟与扩展
  1. synchronized   适合简单的同步需求,但可能会影响性能。
  2. AtomicInteger 是高并发情况下的最佳选择,简洁且性能高。
  3. ReentrantLock 适合需要复杂同步机制的场景,可以提供比 synchronized 更细粒度的控制。

这些方法各有优缺点,具体选择取决于你的需求。通过上述解决方案,能够有效避免并发情况下计数器的错误计算,保证系统的线程安全性和稳定性。

标签:count,Java,++,int,计数器,线程,public
From: https://blog.csdn.net/m0_64307465/article/details/141642850

相关文章

  • Modern C++——不准确“类型声明”引发的非必要性能损耗
    大纲案例代码地址C++是一种强类型语言。我们在编码时就需要明确指出每个变量的类型,进而让编译器可以正确的编译。看似C++编译器比其他弱类型语言的编译器要死板,实则它也做了很多“隐藏”的操作。它会在尝试针对一些非预期类型进行相应转换,以符合预期,比如《C++拾趣——......
  • [Java手撕]交替打印0-100
    两个线程交替打印0-100importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassMain{publicstaticReentrantLocklock=newReentrantLock();publicstaticConditionodd=lock.newCondition();p......
  • [Java手撕]循环打印ABC
    多线程循环打印ABCimportjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassMain{publicstaticReentrantLockLock=newReentrantLock();publicstaticConditionConditionA=Lock.newCondition();......
  • c++ I/O
    1.flush刷新缓存,endl刷新缓存并换行cout<<"Hello"<<fulsh;cout<<"Wait<<endl;2.hex,oct,dec输出16进制,8进制,10进制cout<<hexcout<<octcout<<dec3.使用width调节宽度cout.width(12);//width函数只影响下一个要显示的item4.使用fill填充字符。C++默认......
  • gcc/g++编译ZR
    编译工具链我们写程序的时候用的都是集成开发环境(IDE:IntegratedDevelopmentEnvironment),集成开发环境可以极大地方便我们程序员编写程序,但是配置起来也相对麻烦。在Linux环境下,我们用的是编译工具链,又叫软件开发工具包(SDK:SoftwareDevelopmentKit)。Linux环境下常见......
  • C++ 标准输入输出 -- <iostream>
    <iostream>库是C++标准库中用于输入输出操作的头文件。<iostream>定义了几个常用的流类和操作符,允许程序与标准输入输出设备(如键盘和屏幕)进行交互。以下是<iostream>库的详细使用说明,包括其主要类和常见用法示例。主要类std::istream:用于输入操作的抽象基类。std::ostre......
  • JAVA高级编程之集合框架和泛型(超详细)
    Java集合框架包含的内容Java集合框架提供了一套性能优良、使用方便的接口和类,它们位于java.util包中Collection接口存储一组不唯一,无序的对象List接口存储一组不唯一,有序(插入顺序)的对象Set接口存储一组唯一,无序的对象Map接口存储一组键值对象,提供key到value的映......
  • Java-数据结构-ArrayList-练习 ψ(*`ー´)ψ
    目录:一、练习一(删除str1中str2出现的元素):二、练习二(杨辉三角):三、练习三(简单的洗牌算法):总结:一、练习一(删除str1中str2出现的元素):我们先来看看这个题的条件是什么和如何去做:我们来看代码是什么样的:publicstaticvoidmain(String[]args){//练习1......
  • Java并发编程面试必备:如何创建线程池、线程池拒绝策略
    一、线程池1.线程池使用1.1如何配置线程池大小如何配置线程池大小要看业务系统执行的任务更多的是计算密集型任务,还是I/O密集型任务。大家可以从这两个方面来回答面试官。(1)如果是计算密集型任务,通常情况下,CPU个数为N,设置N+1个线程数量能够实现最优的资源利用率。因为N......
  • 基于Java+SpringBoot+Mysql在线众筹系统功能设计与实现一
    一、前言介绍:1.1项目摘要随着互联网的普及和人们消费观念的转变,众筹作为一种创新的融资方式,逐渐受到社会各界的关注和青睐。它打破了传统融资模式的限制,为初创企业、艺术家、公益项目等提供了更为灵活和便捷的融资渠道。因此,开发众筹系统旨在满足这一市场需求,促进创新项......