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

如何解决资源的限制?

  • 花钱买更高级的机器
  • 根据资源限制并发度
相关文章
|
12天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
20 0
|
18天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
45 14
|
28天前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
58 6
|
23天前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
27 6
|
18天前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
38 0
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
53 4
|
2月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
89 2