Java并发编程的艺术(一)——并发编程需要注意的问题

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34173549/article/details/79612496 并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34173549/article/details/79612496

并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题:

  • 上下文切换
  • 死锁
  • 资源限制

接下来会逐一分析这三个问题,并给出相应的解决方案。

问题一:上下文切换会带来额外的开销

线程的运行机制

  • 一个CPU每个时刻只能执行一条线程;
  • 操作系统给每条线程分配不同长度的时间片;
  • 操作系统会从一堆线程中随机选取一条来执行;
  • 每条线程用完自己的时间片后,即使任务还没完成,操作系统也会剥夺它的执行权,让另一条线程执行

什么是“上下文切换”?

当一条线程的时间片用完后,操作系统会暂停该线程,并保存该线程相应的信息,然后再随机选择一条新线程去执行,这个过程就称为“线程的上下文切换”。

上下文切换的过程

  • 暂停正在执行的线程;
  • 保存该线程的相关信息(如:执行到哪一行、程序计算的中间结果等)
  • 从就绪队列中随机选一条线程;
  • 读取该线程的上下文信息,继续执行

上下文切换是有开销的

每次进行上下文切换时都需要保存当前线程的执行状态,并加载新线程先前的状态。 
如果上下文切换频繁,CPU花在上下文切换上的时间占比就会上升,而真正处理任务的时间占比就会下降。 
因此,为了提高并发程序的执行效率,让CPU把时间花在刀刃上,我们需要减少上下文切换的次数。

如何减少上下文切换?

  • 减少线程的数量 
    由于一个CPU每个时刻只能执行一条线程,而傲娇的我们又想让程序并发执行,操作系统只好不断地进行上下文切换来使我们从感官上觉得程序是并发执的行。因此,我们只要减少线程的数量,就能减少上下文切换的次数。 
    然而如果线程数量已经少于CPU核数,每个CPU执行一条线程,照理来说CPU不需要进行上下文切换了,但事实并非如此。

  • 控制同一把锁上的线程数量 
    如果多条线程共用同一把锁,那么当一条线程获得锁后,其他线程就会被阻塞;当该线程释放锁后,操作系统会从被阻塞的线程中选一条执行,从而又会出现上下文切换。 
    因此,减少同一把锁上的线程数量也能减少上下文切换的次数。

  • 采用无锁并发编程 
    我们知道,如果减少同一把锁上线程的数量就能减少上下文切换的次数,那么如果不用锁,是否就能避免因竞争锁而产生的上下文切换呢? 
    答案是肯定的!但你需要根据以下两种情况挑选不同的策略:

    1. 需要并发执行的任务是无状态的:HASH分段 
      所谓无状态是指并发执行的任务没有共享变量,他们都独立执行。对于这种类型的任务可以按照ID进行HASH分段,每段用一条线程去执行。
    2. 需要并发执行的任务是有状态的:CAS算法 
      如果任务需要修改共享变量,那么必须要控制线程的执行顺序,否则会出现安全性问题。你可以给任务加锁,保证任务的原子性与可见性,但这会引起阻塞,从而发生上下文切换;为了避免上下文切换,你可以使用CAS算法, 仅在线程内部需要更新共享变量时使用CAS算法来更新,这种方式不会阻塞线程,并保证更新过程的安全性。

问题二:并发不当可能会产生死锁

什么是“死锁”?

当多个线程相互等待已经被对方占用的资源时,就会产生死锁。

死锁示例

class DeadLock {
    // 锁A 
    private Object lockA;
    // 锁B
    private Object lockB;

    // 第一条线程
    Thread t1 = new Thread(new Runnable(){
        void run () {
            synchronized (lockA) {
                Thread.sleep(5000);
                synchronized (lockB) {
                    System.out.println("线程1");
                }
            }
        }
    }).start();

    // 第二条线程
    Thread t2 = new Thread(new Runnable(){
        void run () {
            synchronized (lockB) {
                Thread.sleep(5000);
                synchronized (lockA) {
                    System.out.println("线程2");
                }
            }
        }
    }).start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 线程1和线程2都需要锁A和锁B
  • 线程1首先获得锁A,然后sleep 5秒 
    PS:线程sleep过程中会释放执行权
  • 此时线程2执行,获得锁B,然后也sleep 5秒;
  • 线程1 sleep 5秒后继续执行,此时需要锁B,然而锁B已经被线程2持有,因此线程1被阻塞;
  • 此时线程2醒了,它需要锁A,然而锁A已经被线程1持有,因此它也被阻塞;
  • 此时死锁出现了!两条线程相互等待已经被占用的资源,程序就死在这了。 
    死锁是并发编程中一个重要的问题,上面介绍的减少上下文切换只是为了提升程序的性能,而一旦产生死锁,程序就不能正确执行!

如何避免死锁?

  • 不要在一条线程中嵌套使用多个锁;
  • 不要在一条线程中嵌套占用多个计算机资源;
  • 给锁和资源加超时时间 
    如果你非要在一条线程中嵌套使用多个锁或占用多个资源,那你需要给锁、资源加超时时间,从而避免无限期的等待。

问题三:计算机资源会限制并发

误区:线程越多速度越快

在并发编程中,并不是线程越多越好,有时候线程多了反而会拉低执行效率,原因如下:

  • 线程多了会导致上下文切换增多,CPU花在上下文切换的时间增多后,花在处理任务上的时间自然就减少了。
  • 计算机资源会限制程序的并发度。 
    • 比如:你家网入口带宽10M,你写了个多线程下载的软件,同时开100条线程下载,那每条线程平均以每秒100k的速度下载,然而100条线程之间还要不断进行上下文切换,所以你还不如只开5条线程,每条平均2M/s的速度下载。
    • 再比如:数据库连接池最多给你用10个连接,然而你却开了100条线程进行数据库操作,那么当10个用完后其他线程就要等待,从而操作系统要在这100条线程间不断进行上下文切换;所以与其这样还不如只开10条线程,减少上下文切换的次数。

说了这么多只想告诉你一个道理:线程并不是越多越好,要根据当前计算机所能提供的资源考虑。

什么是“资源”?

资源分为硬件资源和软件资源:

  • 硬件资源 
    • 硬盘读写速度
    • 网络带宽
  • 软件资源 
    • Socket连接数
    • 数据库连接数

如何解决资源的限制?

  • 花钱买更高级的机器
  • 根据资源限制并发度
相关文章
|
10月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
471 1
|
10月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
339 6
|
10月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
369 0
|
11月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
1452 2
|
11月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
644 100
|
11月前
|
NoSQL Java 关系型数据库
超全 Java 学习路线,帮你系统掌握编程的超详细 Java 学习路线
本文为超全Java学习路线,涵盖基础语法、面向对象编程、数据结构与算法、多线程、JVM原理、主流框架(如Spring Boot)、数据库(MySQL、Redis)及项目实战等内容,助力从零基础到企业级开发高手的进阶之路。
698 1
|
11月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
631 16
|
12月前
|
安全 Java Shell
Java模块化编程(JPMS)简介与实践
本文全面解析Java 9模块化系统(JPMS),帮助开发者解决JAR地狱、类路径冲突等常见问题,提升代码的封装性、性能与可维护性。内容涵盖模块化核心概念、module-info语法、模块声明、实战迁移、多模块项目构建、高级特性及最佳实践,同时提供常见问题和面试高频题解析,助你掌握Java模块化编程精髓,打造更健壮的应用。
|
12月前
|
安全 算法 Java
Java泛型编程:类型安全与擦除机制
Java泛型详解:从基础语法到类型擦除机制,深入解析通配符与PECS原则,探讨运行时类型获取技巧及最佳实践,助你掌握泛型精髓,写出更安全、灵活的代码。
|
12月前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
1279 3