Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【JUC基础】17. 并发编程常见问题

【JUC基础】17. 并发编程常见问题

作者头像
有一只柴犬
发布于 2024-01-25 03:08:56
发布于 2024-01-25 03:08:56
17500
代码可运行
举报
文章被收录于专栏:JUCJUC
运行总次数:0
代码可运行

1、前言

多线程固然可以提升系统的吞吐量,也可以最大化利用系统资源,提升相应速度。但同时也提高了编程的复杂性,也提升了程序调试的门槛。今天就来汇总一些常见的并发编程中的问题。

2、上下文切换问题

2.1、什么是上下文切换

上下文切换是指在多任务环境下,从一个任务(线程或进程)切换到另一个任务时,保存当前任务的状态(上下文)并加载下一个任务的状态的过程。在操作系统中,上下文切换是实现多任务调度的重要机制之一。当系统中存在多个任务需要并发执行时,操作系统通过快速地切换任务的上下文来实现任务的交替执行,以使每个任务都能得到充分的执行时间。

2.2、上下文切换过程

当一个任务被切换出去时,操作系统会保存当前任务的上下文信息,包括寄存器的值、堆栈指针和程序计数器等。然后,操作系统会加载下一个任务的上下文信息,并将控制权转移到该任务中,使其继续执行。这个过程涉及到保存和恢复大量的寄存器状态以及修改内核数据结构,因此,上下文切换是一个相对耗时的操作。

2.3、上下文切换的原因

上下文切换的主要原因包括:

  1. 时间片轮转:操作系统采用时间片轮转调度算法,每个任务被分配一段时间片进行执行,当时间片用完后,任务被切换出去,切换到下一个任务。
  2. 中断处理:当硬件设备发生中断请求时,当前任务会被中断,操作系统需要立即处理中断请求,因此会发生上下文切换。
  3. 等待事件:当任务需要等待某些事件的发生,如等待用户输入、等待IO操作完成等,任务会被阻塞,操作系统会切换到另一个可执行的任务。

2.4、上下文切换的开销和影响

上下文切换虽然是操作系统实现并发的重要机制,但是它也带来了一些开销和影响:

  1. 时间开销:上下文切换需要保存和恢复大量的上下文信息,涉及到寄存器状态的保存和恢复,以及内核数据结构的修改,因此会消耗一定的处理器时间。
  2. 系统资源消耗:上下文切换涉及到内核数据结构的修改和维护,会占用一定的系统资源,包括内存、处理器等。
  3. 性能下降:频繁的上下文切换会导致系统性能下降,特别是在任务数量较多、切换频率较高的情况下。

正因为上下文切换也会有资源的开销,因此多线程开发中并不是线程数量开得越多越好。

2.5、注意事项和改进策略

当涉及到上下文切换时,以下是一些需要注意的事项和改进策略,并通过Java代码示例进行说明:

  • 减少线程数量:

上下文切换的主要开销来自于保存和恢复线程的上下文信息,因此减少线程数量可以减少上下文切换的次数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ExecutorService executor = Executors.newFixedThreadPool(4); // 使用固定大小的线程池
  • 避免过度线程同步:

过度的线程同步可能导致线程频繁地进入和退出临界区,增加了上下文切换的频率。避免不必要的锁和同步机制。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
AtomicInteger counter = new AtomicInteger(0); // 使用原子操作类避免锁竞争
  • 使用非阻塞算法:

非阻塞算法可以减少对共享资源的竞争,避免线程因为等待资源而阻塞,从而减少上下文切换的次数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>(); // 使用非阻塞队列
  • 优化任务调度:

合理的任务调度策略可以减少上下文切换的次数。例如,将相互依赖的任务放在同一个线程中执行,减少线程间的切换。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ForkJoinPool pool = new ForkJoinPool(); // 使用ForkJoinPool进行任务调度
  • 异步编程模型:

使用异步编程模型可以减少线程的阻塞和等待,从而减少上下文切换的发生。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> computeResult()); // 使用CompletableFuture实现异步编程

通过合理的线程池配置、避免过度同步、使用非阻塞算法、优化任务调度和采用异步编程模型,可以降低上下文切换的频率和开销,提高并发程序的性能和效率。但需要注意,在实际开发中,需要根据具体情况选择适当的策略,并进行性能测试和调优以获得最佳的结果。

3、死锁问题

3.1、什么是死锁

死锁是并发编程中常见的问题,指两个或多个线程彼此持有对方所需的资源,并且由于无法继续执行而相互等待的状态。这导致这些线程无法继续执行下去,从而陷入无限等待的状态,进而影响程序的性能和可靠性。

典型的死锁场景通常涉及以下条件的交叉发生:

  1. 互斥条件:至少有一个资源被视为临界资源,一次只能被一个线程占用。
  2. 请求与保持条件:线程在持有至少一个资源的同时,又请求其他资源。
  3. 不可剥夺条件:已分配的资源不能被其他线程强行夺走。
  4. 循环等待条件:存在一组线程,每个线程都在等待下一个线程所持有的资源。

3.2、死锁示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,两个线程分别尝试获取lock1和lock2的锁,并且获取锁的顺序相反。如果运行该代码,将会导致死锁,因为线程1持有lock1并等待获取lock2,而线程2持有lock2并等待获取lock1,双方相互等待,无法继续执行。

3.3、改进策略

  1. 避免循环等待: 确保线程在获取资源时按照相同的顺序获取,或者使用资源分级,避免循环等待的发生。
  2. 加锁顺序一致性: 线程在获取多个锁时,始终按照相同的顺序获取,避免不同线程之间的锁顺序冲突。
  3. 使用并发库提供的工具:JUC(Java Util Concurrent)包中提供了一些工具类来帮助我们避免死锁的发生,如使用Lock接口及其实现类ReentrantLock代替synchronized关键字进行显式锁定,或者使用java.util.concurrent包中的并发容器来避免手动管理锁。

代码改进:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DeadlockExample {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock1.lock();
            try {
                System.out.println("Thread 1 acquired lock1");
                Thread.sleep(100);
                lock2.lock();
                try {
                    System.out.println("Thread 1 acquired lock2");
                } finally {
                    lock2.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock1.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock2.lock();
            try {
                System.out.println("Thread 2 acquired lock2");
                Thread.sleep(100);
                lock1.lock();
                try {
                    System.out.println("Thread 2 acquired lock1");
                } finally {
                    lock1.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock2.unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

在改进的代码中,我们使用了ReentrantLock来代替synchronized关键字进行显式锁定。这样,我们可以通过调用lock()和unlock()方法来手动管理锁的获取和释放,从而避免死锁的发生。

此外,还有其他一些预防死锁的策略,如:

  1. 资源分配策略: 确保每个线程在请求资源时,能够立即得到所需的资源,而不是无限等待。
  2. 超时策略: 设置一个超时时间,在获取锁或资源的过程中如果超过了该时间仍然无法获取,就放弃并尝试其他方式。
  3. 死锁检测: 可以使用工具来检测死锁的发生,如使用jstack命令查看线程的堆栈信息。

4、竞态条件

竞态条件是指多个线程对共享资源进行操作时,执行的结果依赖于线程执行顺序或时间差的现象。这可能导致不确定的结果或数据一致性问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RaceConditionExample {
    private int count;

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

解决方式:使用Synchronized或ReenterLock。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RaceConditionExample {
    private int count;

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

5、内存可见性

多个线程访问共享变量时,可能会出现内存可见性问题,即一个线程对变量的修改对其他线程不可见。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class VisibilityExample {
    // 解决方法: 使用`volatile`关键字修饰共享变量,保证其对所有线程的可见性。
    // 或者使用`synchronized`关键字或`Lock`接口来确保线程间的同步和数据可见性。
    private  boolean flag = false;

    public void updateFlag() {
        flag = true;
    }

    public void printFlag() {
        while (!flag) {
            // 等待flag变为true
        }
        System.out.println("Flag is true");
    }
}

6、小结

总之,在并发编程中,需要小心处理常见的问题,包括上下文切换的影响、竞态条件、死锁、内存可见性、阻塞和等待以及资源泄漏等。通过合理的同步机制、线程间通信和资源管理,可以提高程序的性能、稳定性和可维护性。同时,通过合理的代码设计和遵循最佳实践。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-01-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java 多线程与并发编程实战指南
随着多核 CPU 的普及,多线程编程已成为现代软件开发的重要技能之一。Java 自 1.0 起便内建了强大的线程支持,并在后续版本中不断增强并发能力:从基本的 Thread、synchronized,到 java.util.concurrent 包,再到 JDK 21 引入的虚拟线程。
用户11690571
2025/06/06
1720
Java 多线程与并发编程实战指南
一文速通JUC中的各种锁
什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改 适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源 synchronized关键字和Lock的实现类都是悲观锁
一个风轻云淡
2023/09/24
2910
关于 java 多线程,你需要知道的一些基础知识
俗话说的好,学好数理化,走遍天下都不怕。也就是说总有一些基础中的基础,学好了这些就可以起到事半功倍的效果
shengjk1
2025/05/16
1000
Java并发编程的艺术,解读并发编程的优缺点
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。 而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能, 过于频繁反而无法发挥出多线程编程的优势。 通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。
李红
2019/05/29
5560
Java并发编程的艺术(一)——并发编程需要注意的问题
并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题: 上下文切换 死锁 资源限制 接下来会逐一分析这三个问题,并给出相应的解决方案。 问题一:上下文切换会带来额外的开销 线程的运行机制 一个CPU每个时刻只能执行一条线程; 操作系统给每条线程分配不同长度的时间片; 操作系统会从一堆线程中随机选取一条来执行; 每条线程用完自己的时间片后,即使任务还没完成,操作系统也会剥夺它的执行权,让另一条线程执行 什么是“上下文
大闲人柴毛毛
2018/03/09
7940
Java并发编程的艺术(一)
并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程就能让程序更大限度地并发执行。--例如上下文切换的问题,死锁的问题,受限于软件和硬件的资源问题。
JathonKatu
2020/10/27
5970
Java并发编程的艺术(一)
2025 年 Java 秋招面试必看的 Java 并发编程面试题汇总
我将从Java并发编程的基础概念、关键机制、工具类等方面入手,为你提供一篇涵盖技术方案与应用实例的文章,助你备战2025年Java秋招面试。
啦啦啦191
2025/06/19
650
2025 年 Java 秋招面试必看的 Java 并发编程面试题汇总
了解Java并发编程基础!超详细!
Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。下面使用JMX来查看一个普通的Java程序包含哪些线程,代码如下。
程序员的时光001
2021/04/02
3660
详解并发编程的优缺点
一直以来并发编程对于刚入行的小白来说总是觉得高深莫测,于是乎,就诞生了想写点东西记录下,以提升理解和堆并发编程的认知。为什么需要用的并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具有哪些缺点?以及在进行并发编程时应该了解和掌握的概念是什么?这篇文章主要以这三个问题来谈一谈。
本人秃顶程序员
2019/05/05
9270
详解并发编程的优缺点
JUC并发知识_并行与并发
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
全栈程序员站长
2022/09/21
3270
JUC并发知识_并行与并发
JUC并发编程
业务:普通的线程代码 Thread Runnable: 没有返回值、效率比Callable 相对较低!
JokerDJ
2023/11/27
3130
JUC并发编程
《java并发编程实战》总结
①发挥多处理器的强大优势 ②建模的简单性 ③异步事件的简化处理④相应更灵敏的用户界面
CBeann
2023/12/25
2730
《java并发编程实战》总结
手把手教你定位常见Java性能问题
性能优化一向是后端服务优化的重点,但是线上性能故障问题不是经常出现,或者受限于业务产品,根本就没办法出现性能问题,包括笔者自己遇到的性能问题也不多,所以为了提前储备知识,当出现问题的时候不会手忙脚乱,我们本篇文章来模拟下常见的几个Java性能故障,来学习怎么去分析和定位。
Guide哥
2020/05/07
1.2K0
手把手教你定位常见Java性能问题
并发编程初探
并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临许多挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战以及解决方案。
JavaEdge
2022/11/29
3520
并发编程初探
杰哥教你面试之一百问系列:java多线程
java多线程是java面试中的高频问题,如何才能在面试中脱颖而出呢?熟读这里的一百个java多线程面试问题即可。
程序那些事
2023/09/01
4190
并发编程基础
并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。
kwan的解忧杂货铺
2024/08/07
1120
高并发编程必备基础(上)
借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,本文算是对多线程情况下同步策略的一个一个简单介绍。
加多
2018/09/06
4630
高并发编程必备基础(上)
解决Java并发编程的难题:死锁
在Java开发中,并发编程是一项常见但也容易引发问题的任务。死锁是其中一个最为棘手的问题,它可能导致应用程序的性能下降或完全停滞。本文将深入探讨死锁的成因,并介绍一些检测和预防死锁的方法。
很酷的站长
2024/01/01
2800
解决Java并发编程的难题:死锁
java 死锁的问题怎么解决的
在 Java 中解决死锁问题通常需要结合代码设计、工具检测和预防策略。以下是详细的解决方案和最佳实践:
用户6556402
2025/05/29
1420
java高并发系列 - 第12天JUC:ReentrantLock重入锁
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:
路人甲Java
2019/12/10
5170
java高并发系列 - 第12天JUC:ReentrantLock重入锁
相关推荐
Java 多线程与并发编程实战指南
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验