关于Java并发多线程的一点思考

news2024/9/24 19:22:20

写在开头

在过去的2023年双11活动中,天猫的累计访问人次达到了8亿,京东超60个品牌销售破10亿,直播观看人数3.0亿人次,订单支付频率1分钟之内可达百万级峰值,这样的瞬间高并发活动,给服务端带来的冲击可想而知,就如同医院那么多医生,去看病挂号时,有时候都需要排队,对于很多时间就是金钱的场景来说,是不可忍受的。

为什么要使用多线程并发

在上述这种场景下我们就不得不去学习多线程下的并发处理,我们先来了解一下并发与线程的概念

并发: 并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件,存在对 CPU 资源进行抢占。

线程: 是进程的子任务,因此它本身不会独立存在,系统不会为线程分配内存,线程组之间只能共享所属进程的资源,而线程仅仅是CPU 调度和分派的基本单位,当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行。

有了这两个概念后,我们再来聊一聊并发多线程的必要性或者它所具有的优势😊

从计算机底层出发:

  1. 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
  2. 多核时代: 随着互联网的深入发展,计算机CPU也进入到了多核时代,举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

从互联网现状出发:

随着科技的进步,互联网上的应用场景更加复杂化,使用互联网的网民也呈指数增长,动辄就是百万甚至千万级的并发吞吐量,这也促进者开发者们不断的提升系统的性能,而提升高并发处理效率的基础就是多线程!

在这里插入图片描述

总结: 基于上述内容,我们可以做如下3点总结:

  1. 在硬件上提高了 CPU 的核数和个数以后,多线程并发可以提升 CPU 的计算能力的利用率。
  2. 多线程并发可以提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
  3. 并发多线程可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务。

多线程会带来什么问题?

上面我们阐述了多线程使用的好处,以及它发展的必然趋势,但这里我们同样还要思考另外一个问题,那就是多线程真的完美无缺的吗?

答案当然是个否命题了,经过多年积累我们发现多线程在使用上其实也存在很多的问题:

  • Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间;
  • 容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致;
  • 多线程容易造成死锁、活锁、线程饥饿、内存泄露等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重;
  • 开发难度相对较高,需要相当开发人员充分的了解多线程,才能开发出高效的并发程序。

探索问题的根本原因

在一个Java程序或者说进程运行的过程中,会涉及到CPU、内存、IO设备,这三者在读写速度上存在着巨大差异:CPU速度-优于-内存的速度-优于-IO设备的速度

为了平衡这三者之间的速度差异,达到程序响应最大化,计算机、操作系统、编译器都做出了自己的努力。

  • 计算机体系结构:给 CPU 增加了缓存,均衡 CPU 和内存的速度差异;
  • 操作系统:增加了进程与线程,分时复用 CPU,均衡 CPU 和 IO 设备的速度差异;
  • 编译器:增加了指令执行重排序(这个也会带来另外的问题,我们在后面的学习中会提到),更好地利用缓存,提高程序的执行速度。

这种优化是充分必要的,但这种优化同时会给多线程程序带来原子性、可见性和有序性的问题。

关于多线程的原子性、可见性、有序性问题

我们接着上面的问题向下深入讨论,先来看看什么是原子性、可见性、有序性。

原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性;
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到;
有序性:程序执行的顺序按照代码的先后顺序执行;

原子性分析

操作系统对当前执行线程的切换,可能会带来了原子性问题。

【代码示例1】

public class Test {
    //计数变量
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 结束");
        });
        //线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 结束");
        });
        //启动线程 1
        t1.start();
        //启动线程 2
        t2.start();
        //等待线程 1 执行完成
        t1.join();
        //等待线程 2 执行完成
        t2.join();
        //打印 count 变量
        System.out.println(count);
    }
}

我们创建了2个线程,分别对count进行加10000操作,理论上最终输出的结果应该是20000万对吧,但实际并不是,我们看一下真实输出。

输出:

thread t1 count 加 10000 结束
thread t2 count 加 10000 结束
14281

原因:
Java 代码中 的 count++ ,至少需要三条CPU指令:

  • 指令 1:把变量 count 从内存加载到CPU的寄存器
  • 指令 2:在寄存器中执行 count + 1 操作
  • 指令 3:+1 后的结果写入CPU缓存或内存

即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。这种情况多发生在cpu占用时间较长的线程中,若单线程对count仅增加100,那我们就很难遇到线程的切换,得出的结果也就是200啦。
在这里插入图片描述
解决办法:
可以通过JDK Atomic开头的原子类、synchronized、LOCK,解决多线程原子性问题,后面的博文会详细分析,这里只给结果哈。

可见性分析

CPU 缓存,在多核 CPU 的情况下,带来了可见性问题。

【代码示例2】

public class Test {
    //是否停止 变量
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        //启动线程 1,当 stop 为 true,结束循环
        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();
        //休眠 1 秒
        Thread.sleep(1000);
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
}

输出:

线程 1 正在运行...
线程 2 正在运行...
设置 stop 变量为 true.

原因:
我们会发现,线程1运行起来后,休眠1秒,启动线程2,可即便线程2把stop设置为true了,线程1仍然没有停止,这个就是因为 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。
在这里插入图片描述
解决办法:

通过 volatile、synchronized、Lock接口、Atomic 类型保障可见性

有序性分析

编译器指令重排优化,带来了有序性问题。

【代码示例3】

public class Test {

    static int x;//静态变量 x
    static int y;//静态变量 y

    public static void main(String[] args) throws InterruptedException {
        Set<String> valueSet = new HashSet<String>();//记录出现的结果的情况
        Map<String, Integer> valueMap = new HashMap<String, Integer>();//存储结果的键值对

        //循环 1万次,记录可能出现的 v1 和 v2 的情况
        for (int i = 0; i <10000; i++) {
            //给 x y 赋值为 0
            x = 0;
            y = 0;
            valueMap.clear();//清除之前记录的键值对
            Thread t1 = new Thread(() -> {
                int v1 = y;//将 y 赋值给 v1 ----> Step1
                x = 1;//设置 x 为 1  ----> Step2
                valueMap.put("v1", v1);//v1 值存入 valueMap 中  ----> Step3
            }) ;

            Thread t2 = new Thread(() -> {
                int v2 = x;//将 x 赋值给 v2  ----> Step4
                y = 1;//设置 y 为 1  ----> Step5
                valueMap.put("v2", v2);//v2 值存入 valueMap 中  ----> Step6
            });

            //启动线程 t1 t2
            t1.start();
            t2.start();
            //等待线程 t1 t2 执行完成
            t1.join();
            t2.join();

            //利用 Set 记录并打印 v1 和 v2 可能出现的不同结果
            valueSet.add("(v1=" + valueMap.get("v1") + ",v2=" + valueMap.get("v2") + ")");
            System.out.println(valueSet);
        }
    }
}

输出:

...
[(v1=1,v2=0), (v1=0,v2=0), (v1=0,v2=1)]
[(v1=1,v2=0), (v1=0,v2=0), (v1=0,v2=1)]
[(v1=1,v2=0), (v1=0,v2=0), (v1=0,v2=1)]
[(v1=1,v2=0), (v1=0,v2=0), (v1=0,v2=1)]
...

v1=0,v2=0 的执行顺序是 Step1 和 Step 4 先执行

v1=1,v2=0 的执行顺序是 Step5 先于 Step1 执行

v1=0,v2=1 的执行顺序是 Step2 先于 Step4 执行

v1=1,v2=1 出现的概率极低,就是因为 CPU 指令重排序造成的。Step2 被优化到 Step1 前,Step5 被优化到 Step4 前,至少需要成立一个。

解决办法:
Happens-Before 规则可以解决有序性问题,后续会的博文中也会提到。

总结

好啦,关于Java并发多线程的思考就写这么多啦🤗

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

在这里插入图片描述
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

在这里插入图片描述

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

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

相关文章

如何利用会话式AI提升你的工作效率?

会话式AI如何改变我们的生活和工作 在当今时代&#xff0c;内容策略的重要性日渐凸显&#xff0c;良好的内容策略能够与流量及转化率紧密相连&#xff0c;成为企业在内容策略领域不容忽视的营销工具之一。 然而&#xff0c;目前内容同质化现象严重&#xff0c;企业若想在内容营…

爬虫入门到精通_实战篇11(使用代理处理反爬抓取微信文章)_PyQuery使用

1 目标 搜狗-微信这个网址来爬取微信的文章&#xff1a; ps&#xff1a;登录后才能查看第10页之后的内容&#xff1a; 量翻页触发了网站的反爬虫措施&#xff0c;导致ip被封&#xff0c;需要进行解锁。 然而从doc中可以看到&#xff0c;请求失败的那页&#xff08;状态码应…

vs2022 qt 关于lnk2001和2019同时报错的问题

需要像qt中添加模块&#xff0c;这里&#xff0c;缺少qtopenglwidgets模块

向上生长笔记

第一章 成为一个很厉害的人(持续输入&#xff0c;反复练习) 为什么要学习及如何学习 1、自毁趋势(熵增)&#xff0c;故需要能量输入(负熵流) //引申&#xff1a;水往低处流是趋势&#xff0c;学习是逆趋势。 2、持续输入能量&#xff08;物质和信息&#xff09;&#xff0c;…

C++ 根据公式计算椭圆任意点到中心的距离

#include <iostream> using namespace std;double fact(int x) //定义阶乘函数。注意是double类型 {double y x; //注意是double类型for (int i x-1; i > 0; i--)y * i;return y; };double My_sin(int x) //定义sin函数。注意是double类型 {double y 0; //注意是do…

SPI总线知识总结

1 SPI的时钟极性CPOL和时钟相位CPHA的设置 1.1 SPI数据传输位数 SPI传输数据过程中总是先发送或接收高字节数据&#xff0c;每个时钟周期接收器或发送器左移一位数据。对于小于16位的数据&#xff0c;在发送前必须左对齐&#xff0c;如果接收的数据小于16位&#xff0c;则采用软…

wordpress模板官网

移民wordpress主题 移民代办wordpress主题&#xff0c;适合做海外移民咨询的代理公司搭建wordpress企业官方网站使用。 https://www.jianzhanpress.com/?p5130 夏令营wordpress主题 绿色夏令营wordpress主题&#xff0c;适合做夏令营或户外拓展的公司搭建wordpress官方网站…

Go 与 Rust:导航编程语言景观

在当今构建软件时&#xff0c;开发者在编程语言上有着丰富的选择。两种脱颖而出的语言是 Go 和 Rust - 都很强大但却截然不同。本文将从各种因素比较这两种语言&#xff0c;以帮助您确定哪种更适合您的需求。 我们将权衡它们在并发、安全性、速度、互操作性等方面的方法。我们将…

【Redis】Redis入门

文章目录 一、Redis简介二、Redis的特性三、Redis的使用场景四、Redis可以做什么五、Redis不可以做什么六、CentOS7安装Redis5七、Redis通用命令 一、Redis简介 Redis 是⼀种基于 键值对&#xff08;key-value&#xff09; 的NoSQL数据库&#xff0c;与很多键值对数据库不同的…

Win UI3开发笔记(四)设置主题续2

本机深色主题下设置的背景颜色可以作用于整个对话框&#xff0c;本机浅色模式下设置的背景颜色只作用与下边的部分。 如果本机选深色&#xff0c;程序选浅色&#xff0c;设置为light只对上部分管用&#xff0c;下部分不管用。如图&#xff0c;左边那个hello按钮要看不见了。。…

Mac版2024 CleanMyMac X 4.14.6 核心功能详解

CleanMyMac 是 macOS 上久负盛名的系统清理工具&#xff0c;2018 年&#xff0c;里程碑式版本 CleanMyMac X 正式发布。不仅仅是命名上的变化&#xff0c;焕然一新的 UI、流畅的动画也让它显得更加精致。新增的系统优化、软件更新等功能&#xff0c;使得在日常使用 macOS 时有了…

力扣hot100:15.三数之和(双指针/哈希表)

分析&#xff1a; 三数和问题&#xff0c;这里和两数之和不一样&#xff0c;返回的是值&#xff0c;因此可以对其进行排序&#xff0c;使用双指针。 一、一层循环双指针 class Solution { public:vector<vector<int>> threeSum(vector<int>& nums) {sort…

react-beautiful-dnd组件报Unable to find draggable with id

一、问题现象 项目中使用react-beautiful-dnd组件实现可拖拽,但拖了1次后可能会出现拖拽异常(元素拖不动),打开控制台会发现有报错 二、解决方案 给Draggable组件和其下方的div添加了key就正常了,以下是我自己简单写的一个demo,可供参考 import {useState } from reac…

代码随想录算法训练营第32天—贪心算法06 | ● *738.单调递增的数字 ● *968.监控二叉树 ● 总结

*738.单调递增的数字 https://programmercarl.com/0738.%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%95%B0%E5%AD%97.html 考点 贪心算法 我的思路 暴力解法 视频讲解关键点总结 几个关键点一&#xff0c;如果当前数位小于上一数位&#xff0c;如87&#xff0c;则应直…

redis06 redis事务

思维草图 redis事务认识 redis事务是一个单独的隔离操作&#xff0c;事务中的所有命令都会序列化、按顺序地执行&#xff0c;事务在执行的过程中&#xff0c;不会被其他客户端发送来的命令请求所打断。 redis事务的主要作用就是串联多个命令防止别的命令插队。 Multi、Exec、…

AWS 认证报名考试流程

AWS认证的考试包括&#xff0c;可以申请线上或者线下考试。 考试类型 线上&#xff1a; 优点&#xff1a;方便快捷无需通勤&#xff0c;随时约随时考&#xff0c;基本上每天都可以 缺点&#xff1a;对环境要求较高&#xff0c;屋子里只能有自己&#xff0c;而且不能有其他声音…

【Linux】Linux C编程

gcc编译器 gcc [options] [filenames] 其中&#xff0c;options是编译器所需要的选项参数&#xff0c;filenames是文件名。 gcc编译过程 C语言编译过程一般可以分为预处理、编译、汇编、链接四个步骤。 1.预处理阶段 预处理阶段主要处理宏定义和include&#xff0c;并进行语…

腾轩科技传媒讲解企业怎样做整合营销推广才有效果

当今信息爆炸的时代&#xff0c;企业如何才能从浩渺的市场中脱颖而出&#xff0c;触达目标消费者&#xff0c;实现品牌与销售的双增长&#xff1f;腾轩科技传媒讲解的整合营销推广正是解决这一问题的关键所在。它不仅仅是一种营销策略&#xff0c;更是一种全新的思维方式&#…

全面分析vcruntime140_1.dll无法继续执行代码的处理方法,3分钟修复vcruntime140_1.dll

如果系统弹出一个错误警告&#xff0c;指出“vcruntime140_1.dll无法继续执行代码”&#xff0c;这通常意味着您的Windows系统中缺失了一个关键的文件&#xff0c;或者该文件已损坏。​vcruntime140_1.dll​是随Visual C Redistributable for Visual Studio 2015, 2017和2019提…

购物车下单实现思路【vue】

文章目录 1、购物车1.1 使用pinia来存储购物车的状态1.2 用户将商品加入购物车的时候 2、点击结算按钮3、封装支付 1、购物车 在购物车页面会有一个点 提示当前购物车内商品的数量购物车内商品的展示购物车内商品的选中 1.1 使用pinia来存储购物车的状态 state: () > ({//…