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

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
实时计算 Flink 版,5000CU*H 3个月
简介: 多线程固然可以提升系统的吞吐量,也可以最大化利用系统资源,提升相应速度。但同时也提高了编程的复杂性,也提升了程序调试的门槛。今天就来汇总一些常见的并发编程中的问题。

 目录

1、前言

2、上下文切换问题

2.1、什么是上下文切换

2.2、上下文切换过程

2.3、上下文切换的原因

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

2.5、注意事项和改进策略

3、死锁问题

3.1、什么是死锁

3.2、死锁示例

3.3、改进策略

4、竞态条件

5、内存可见性

6、小结


1、前言

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

2、上下文切换问题

image.gif编辑

2.1、什么是上下文切换

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

2.2、上下文切换过程

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

2.3、上下文切换的原因

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

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

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

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

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

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

      2.5、注意事项和改进策略

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

        • 减少线程数量:

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

        ExecutorService executor = Executors.newFixedThreadPool(4); // 使用固定大小的线程池

        image.gif

          • 避免过度线程同步:

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

          AtomicInteger counter = new AtomicInteger(0); // 使用原子操作类避免锁竞争

          image.gif

            • 使用非阻塞算法:

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

            ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>(); // 使用非阻塞队列

            image.gif

              • 优化任务调度:

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

              ForkJoinPool pool = new ForkJoinPool(); // 使用ForkJoinPool进行任务调度

              image.gif

                • 异步编程模型:

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

                CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> computeResult()); // 使用CompletableFuture实现异步编程

                image.gif

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

                3、死锁问题

                image.gif编辑

                3.1、什么是死锁

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

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

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

                  3.2、死锁示例

                  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();
                      }
                  }

                  image.gif

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

                  3.3、改进策略

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

                    代码改进:

                    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();
                        }
                    }

                    image.gif

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

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

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

                      4、竞态条件

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

                      public class RaceConditionExample {
                          private int count;
                          public void increment() {
                              count++;
                          }
                      }

                      image.gif

                      解决方式:使用Synchronized或ReenterLock。

                      public class RaceConditionExample {
                          private int count;
                          public synchronized void increment() {
                              count++;
                          }
                      }

                      image.gif

                      5、内存可见性

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

                      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");
                          }
                      }

                      image.gif

                      6、小结

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

                      相关文章
                      |
                      数据采集 存储 SQL
                      数据中台全景架构及模块解析!一文入门中台架构师!
                      数据中台全景架构及模块解析!包括数据采集、数据存储、数据开发处理、数据资产管理、数据质量和安全、数据服务。一文入门中台架构师!
                      |
                      NoSQL MongoDB
                      mongodb 分组查询、指定时间段查询
                      mongodb 分组查询、指定时间段查询
                      |
                      9月前
                      |
                      运维 监控 Linux
                      推荐几个不错的 Linux 服务器管理工具
                      推荐几个不错的 Linux 服务器管理工具
                      599 6
                      |
                      10月前
                      |
                      中间件 编译器 开发工具
                      如何用易语言进行跨平台的软件开发
                      如何用易语言进行跨平台的软件开发
                      128 3
                      |
                      11月前
                      |
                      前端开发 JavaScript API
                      前端的全栈之路Meteor篇(完):关于前后端分离及与各框架的对比,浅析分离之下的潜在耦合
                      本文探讨了Meteor.js这一全栈JavaScript框架的特点与优势,特别是在前后端分离架构中的应用。Meteor通过共享数据结构和简化全栈开发流程,实现了前后端的紧密协作。文章还对比了其他全栈框架,如Next.js、Nuxt.js等,分析了各自的优势与适用场景,最后讨论了通过定义文档归属者和用户专有数据集简化后端构建及端云数据同步的方法。
                      713 0
                      |
                      前端开发 中间件 数据处理
                      MVVM模式的具体实现
                      MVVM模式的具体实现
                      170 0
                      |
                      机器学习/深度学习 自然语言处理 算法
                      基于卷积神经网络(CNN)的垃圾邮件过滤方法
                      传统的垃圾邮件过滤手段如规则匹配常因垃圾邮件的多变而失效。基于深度学习的方法,特别是卷积神经网络(CNN),能自动学习邮件中的复杂特征,有效识别垃圾邮件的新形态。CNN通过特征学习、处理复杂结构、良好的泛化能力和适应性,以及高效处理大数据的能力,显著提升了过滤精度。在文本分类任务中,CNN通过卷积层提取局部特征,池化层减少维度,全连接层进行分类,特别适合捕捉文本的局部模式和顺序信息,从而构建高效的垃圾邮件过滤系统。
                      816 0
                      |
                      存储 分布式计算 数据处理
                      MaxCompute 的成本效益分析与优化策略
                      【8月更文第31天】随着云计算技术的发展,越来越多的企业选择将数据处理和分析任务迁移到云端。阿里云的 MaxCompute 是一款专为海量数据设计的大规模数据仓库平台,它不仅提供了强大的数据处理能力,还简化了数据管理的工作流程。然而,在享受这些便利的同时,企业也需要考虑如何有效地控制成本,确保资源得到最优利用。本文将探讨如何评估 MaxCompute 的使用成本,并提出一些优化策略以降低费用,提高资源利用率。
                      452 0
                      |
                      监控 安全 Cloud Native
                      零信任安全模型:构建未来数字世界的安全基石
                      在数字化转型的浪潮中,云原生技术已成为推动企业创新和灵活性的关键力量💡。然而,随着技术的进步和应用的广泛,网络安全威胁也日益严峻🔓,传统的网络安全模型已经难以应对复杂多变的网络环境。在这样的背景下,零信任安全模型(Zero Trust)应运而生,成为提升网络安全防护能力的重要策略🛡️。本文将深入探讨零信任的概念、必要性、以及它如何解决传统网络模型面临的痛点和挑战。
                      零信任安全模型:构建未来数字世界的安全基石
                      |
                      域名解析 SQL 监控
                      Web Web Application Firewall
                      阿里云Web应用防火墙(Web Application Firewall,简称WAF)是一种网络安全服务,用于保护Web应用程序免受常见的Web攻击,如SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)等。它可以通过配置规则和策略,识别和拦截恶意流量,从而保护Web应用程序的安全。
                      356 1