java-面试- Java并发编程(一)——并发编程需要注意的问题

简介: 并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题:上下文切换死锁资源限制接下来会逐一分析这三个问题,并给出相应的解决方案。

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

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

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

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

线程的运行机制

  • 一个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连接数
    • 数据库连接数

如何解决资源的限制?

  • 花钱买更高级的机器
  • 根据资源限制并发度
相关文章
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
5天前
|
XML 缓存 Java
Java大厂面试题
Java大厂面试题
18 0
|
5天前
|
存储 安全 Java
Java大厂面试题
Java大厂面试题
11 0
|
5天前
|
存储 安全 Java
Java大厂面试题
Java大厂面试题
13 0
|
5天前
|
IDE Java 物联网
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
13 0
|
5天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
6天前
|
安全 Java
就只说 3 个 Java 面试题 —— 02
就只说 3 个 Java 面试题 —— 02
18 0
|
6天前
|
存储 安全 Java
就只说 3 个 Java 面试题
就只说 3 个 Java 面试题
10 0
|
6天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
6天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。