写在开头

在过去的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成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

关于Java并发多线程的一点思考的更多相关文章

  1. java并发多线程显式锁Condition条件简介分析与监视器 多线程下篇(四)

    Lock接口提供了方法Condition newCondition();用于获取对应锁的条件,可以在这个条件对象上调用监视器方法 可以理解为,原本借助于synchronized关键字以及锁对象,配备了 ...

  2. java 并发多线程 锁的分类概念介绍 多线程下篇(二)

    接下来对锁的概念再次进行深入的介绍 之前反复的提到锁,通常的理解就是,锁---互斥---同步---阻塞 其实这是常用的独占锁(排它锁)的概念,也是一种简单粗暴的解决方案 抗战电影中,经常出现为了阻止日 ...

  3. java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)

    目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性.可见性.有序性 对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方 ...

  4. Java并发多线程 - 并发工具类JUC

    安全共享对象策略 1.线程限制 : 一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改 2.共享只读 : 一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问, 但是任何线程都 ...

  5. Java并发/多线程系列——初识篇

    回到过去,电脑有一个CPU,一次只能执行一个程序.后来多任务处理意味着计算机可以同时执行多个程序(AKA任务或进程).这不是真的"同时".单个CPU在程序之间共享.操作系统将在运行 ...

  6. day 04 Java并发多线程

    http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referralPS:而JVM 每遇到 ...

  7. Java并发(一)Java并发/多线程教程

    在过去一台电脑只有单个CPU,并且在同一时间只能执行单个程序.后来出现的"多任务"意味着电脑在可以同时执行多个程序(AKA任务或者进程).虽然那并不是真正意义上的"同时& ...

  8. Java并发多线程面试题 Top 50

    不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎.大多数待遇丰厚的Java开发职位都要求开发者精通多线程 ...

  9. Java并发/多线程系列——线程安全篇(1)

    创建和启动Java线程 Java线程是个对象,和其他任何的Java对象一样.线程是类的实例java.lang.Thread,或该类的子类的实例.除了对象之外,java线程还可以执行代码. 创建和启动线 ...

  10. Java并发-多线程面试(全面)

    1. 什么是线程?2. 什么是线程安全和线程不安全?3. 什么是自旋锁?4. 什么是Java内存模型?5. 什么是CAS?6. 什么是乐观锁和悲观锁?7. 什么是AQS?8. 什么是原子操作?在Jav ...

随机推荐

  1. 【解决一个小问题】macbook m2 上交叉编译 gozstd

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 已知 zstd 是一个优秀的压缩库,gozstd封装了这个 ...

  2. mac中virtualBox添加主机网络报错VBoxNetAdpCtl: Error while adding new interface: failed to open /dev/vboxnetctl: No such file or directory

    mac中virtualBox添加主机网络报错 现场复原 解决方法 参考 mac中virtualBox添加主机网络报错 现场复原 virtual box添加主机网络报错 VBoxNetAdpCtl: E ...

  3. 强化学习从基础到进阶-案例与实践[3]:表格型方法:Sarsa、Qlearning;蒙特卡洛策略、时序差分等以及Qlearning项目实战

    强化学习从基础到进阶-案例与实践[3]:表格型方法:Sarsa.Qlearning:蒙特卡洛策略.时序差分等以及Qlearning项目实战 策略最简单的表示是查找表(look-up table),即表 ...

  4. 【算法】C语言程序编程模拟实现strlen函数和strcpy函数

    C语言程序编程模拟实现strlen函数和strcpy函数(超详细的注释和解释) 求个赞求个赞求个赞求个赞 谢谢 先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说 ...

  5. SecureCRT终端显示中文乱码问题的解决方案

    错误描述:在windows10下通过SecureCRT远程连接Linux终端时,如果Linux里面存放有带有中文的文件或文件名,那么SecureCRT终端就会显示乱码. 错误记录:如下图所示,在我的文 ...

  6. 蓝鲸:安装SaaS组件bk_monitor失败分析解决

    使用./bk_install saas-o 安装发现bk_monitor(蓝鲸监控)组件报错"ERROR deploy failed: timeout". 单独尝试安装各个组件: ...

  7. 《ASP.NET Core 微服务实战》-- 读书笔记(第9章)

    第 9 章 微服务系统的配置 微服务系统中的配置需要关注更多其他方面的因素,包括: 配置值的安全读写 值变更的审计能力 配置信息源本身的韧性和可靠性 少量的环境变量难以承载大型.复杂的配置信息 应用要 ...

  8. Linux防火墙操作命令(开放或关闭端口)

    在外部访问CentOS中部署应用时,需要通过防火墙管理软件,开端口,或者直接关闭防火墙进行解决(不建议) 常用命令:systemctl start firewalld               #启 ...

  9. CF131D Subway 题解

    题目传送门 前置知识 强连通分量 | 最短路 解法 考虑用 Tarjan 进行缩点,然后跑最短路. 缩点:本题的缩点有些特殊,基于有向图缩点修改而得,因为是无向图,所以在 Tarjan 过程中要额外记 ...

  10. Linux下磁盘管理工具:hdparm/iostat/parted/fdiisk/badblocks/smartctl/losetup/sg3_utils/sqinfo/smp_utils/udevadm

    一.hdparm:       hdparm可以检测,显示与设定IDE,SCSI,SATA,SAS硬盘的硬件参数,       如: hdparm -I /dev/sdc 可以获取sdc的硬件信息  ...