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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JVM揭秘之旅:打破性能瓶的终极指南(1)
「为什么Java程序员必须啃透JVM?」 JVM是Java生态的“灵魂引擎”,但多数开发者仅停留在API调用层面。当面临频发GC卡顿、诡异OOM崩溃或线程死锁顽疾时,是否曾因底层原理的模糊而束手无策?本专栏将带您穿透技术迷雾,系统攻克JVM核心领域:
半旧518
2025/07/03
850
一文带你读懂String类源码
String 是日常开发非常频繁的类,此外我们常用的操作还有字符串连接操作符等等。String对象是不可变的,查看JDK文档,我们不难发现String类的每个修改值的方法,其实都是创建了一个新的String对象,以包含修改后的字符串内容。
后台技术汇
2022/05/28
3520
一文带你读懂String类源码
从字符串到常量池,一文看懂String类
这道题就算你没做过也肯定看到,总所周知,它创建了两个对象,一个位于堆上,一个位于常量池中。
cxuan
2020/06/28
1K0
从字符串到常量池,一文看懂String类
Java面试- JVM 内存模型讲解
经常有人会有这么一个疑惑,难道 Java 开发就一定要懂得 JVM 的原理吗?我不懂 JVM ,但我照样可以开发。确实,但如果懂得了 JVM ,可以让你在技术的这条路上走的更远一些。
健程之道
2019/11/02
8390
字符串性能优化不容小觑
String对象是我们日常工作中使用最频繁的对象,它的性能问题也是我们最容易忽略的。String对象作为Java语言中最重要的数据类型,是内存中占据空间最大的对象,高效地使用字符串,可以提升系统的整体性能。
故里
2020/11/25
5790
字符串性能优化不容小觑
这次让我们从字节码文件来重新认识String,文末有两个小小面试题,一起来试一试
String 实现了Serializable和Comparable接口:即字符串是支持序列化和比较大小的。
宁在春
2022/10/31
3490
这次让我们从字节码文件来重新认识String,文末有两个小小面试题,一起来试一试
String、StringBuilder、StringBuffer区别;String底层详解,实例化、拼接、比较;String为什么不可变
String是Java中的一个内置类,Immutable不可变,即一旦创建String对象,它的值就不能被更改。对String对象的replace、subString、toLowerCase等操作都会返回一个新String对象,故每次操作String时 性能较低、浪费内存空间
寻求出路的程序媛
2024/04/21
3520
String、StringBuilder、StringBuffer区别;String底层详解,实例化、拼接、比较;String为什么不可变
jvm之StringTable解读
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
一个风轻云淡
2023/10/15
2910
jvm之StringTable解读
Java String 对象,你真的了解了吗?
String对象是 Java 中使用最频繁的对象之一,所以 Java 公司也在不断的对String对象的实现进行优化,以便提升String对象的性能,看下面这张图,一起了解一下String对象的优化过程。
平头哥的技术博文
2019/09/24
8580
面试必考java字符串String
众所周知在java里面除了8种基本数据类型的话,还有一种特殊的类型String,这个类型是我们每天搬砖都基本上要使用它。
java金融
2020/11/29
5160
面试必考java字符串String
JVM - 深入剖析字符串常量池
看 1.8 , 疯狂的intern, 抛出了 heap oom ,由此可以推断出 1.8中的字符串常量池 是在堆中。
小小工匠
2021/08/17
6460
彻底弄懂java中的常量池
class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。 class文件中存在常量池(非运行时常量池),其在编译阶段就已经确定,jvm规范对class文件结构有着严格的规范,必须符合此规范的class文件才能被jvm任何和装载。为了方便说明,我们写个简单的类
秃头哥编程
2019/06/24
22.8K13
彻底弄懂java中的常量池
Android经典面试题笔记之JVM内存管理剖析
class文件通过类加载器加载到运行时数据区,运行时数据区又分为线程私有和线程共享的内存;
AntDream
2024/10/08
1360
Android经典面试题笔记之JVM内存管理剖析
彻底弄懂java中的常量池
JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
用户8062311
2021/01/10
1K0
JVM运行时数据区(<=JDK7 and JDK8+)
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
斯武丶风晴
2020/04/24
7750
JVM运行时数据区(<=JDK7  and  JDK8+)
Java String Krains 2020-08-05
String源码中是这样定义的,String底层在jdk8及以前是用char数组存储的,而jdk9之后改用byte数组存储,由于都加了final关键字,String是不可变的。
Krains
2020/08/10
3950
Java String  Krains 2020-08-05
Java String 演进全解析
String 是我们使用最频繁的对象,使用不当会对内存、程序的性能造成影响,本篇文章全面介绍一下 Java 的 String 是如何演进的,以及使用 String 的注意事项。
Yano_nankai
2020/11/29
3790
Java String 演进全解析
再议String-字符串常量池与String.intern()
来源:blog.csdn.net/gcoder_/article/details/106644312
Java小咖秀
2021/08/05
3740
再议String-字符串常量池与String.intern()
阿里一面:如何将重复性比较高的 String 类型的地址信息从 20GB 降到几百兆?
这次应该是互联网及软件行业的第三次寒潮,大家在寒潮中一定要继续保持学习,寒潮挺过去以后还是会迎来新的发展机遇。
码哥字节
2024/04/13
1520
阿里一面:如何将重复性比较高的 String 类型的地址信息从 20GB 降到几百兆?
JVM 学习笔记(1):Java内存区域
程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器 ,也就是记录下 Java 程序当前指令的地址偏移量,可在线程切换时记录下当前线程执行的位置,给 CPU 提供指令地址,以便下一次切换回来找到继续执行的位置。
玛卡bug卡
2022/09/20
4970
JVM 学习笔记(1):Java内存区域
推荐阅读
相关推荐
JVM揭秘之旅:打破性能瓶的终极指南(1)
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验