Java性能优化六、多线程调优1

首图

多线程调优1

一、多线程调优(上):哪些操作导致了上下文切换?

1、初识上下文切换

其实在单个处理器的时期,操作系统就能处理多线程并发任务。处理器给每个线程分配 CPU 时间片(Time Slice),线程在分配获得的时间片内执行任务。

时间片决定了一个线程可以连续占用处理器运行的时长。当一个线程的时间片用完了,或者因自身原因被迫暂停运行了,这个时候,另外一个线程(可以是同一个线程或者其它进程的线程)就会被操作系统选中,来占用处理器。这种一个线程被暂停剥夺使用权,另外一个线程被选中开始或者继续运行的过程就叫做上下文切换(Context Switch)。

具体来说:

  • 一个线程被剥夺处理器的使用权而被暂停运行,就是“切出”;
  • 一个线程被选中占用处理器开始或者继续运行,就是“切入”。

在这种切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是“上下文”了。那上下文都包括哪些内容呢?具体来说,它包括了寄存器的存储内容以及程序计数器存储的指令内容。CPU 寄存器负责存储已经、正在和将要执行的任务,程序计数器负责存储 CPU 正在执行的指令位置以及即将执行的下一条指令的位置

在当前 CPU 数量远远不止一个的情况下,操作系统将 CPU 轮流分配给线程任务,此时的上下文切换就变得更加频繁了,并且存在跨 CPU 上下文切换,比起单核上下文切换,跨核切换更加昂贵。

2、多线程上下文切换诱因

在操作系统中,上下文切换的类型还可以分为进程间的上下文切换和线程间的上下文切换。而在多线程编程中,我们主要面对的就是线程间的上下文切换导致的性能问题。

下面我们就重点看看究竟是什么原因导致了多线程的上下文切换。

开始之前,先看下 Java 线程的生命周期状态。

image-20210630093640579

结合图示可知,线程主要有“新建”(NEW)、“就绪”(RUNNABLE)、“运行”(RUNNING)、“阻塞”(BLOCKED)、“死亡”(DEAD)五种状态。

一个线程的状态由 RUNNING 转为 BLOCKED ,再由BLOCKED 转为 RUNNABLE ,然后再被调度器选中执行,这就是一个上下文切换的过程。

  • 当一个线程从 RUNNING 状态转为 BLOCKED 状态时,我们称为一个线程的暂停,线程暂停被切出之后,操作系统会保存相应的上下文,以便这个线程稍后再次进入 RUNNABLE 状态时能够在之前执行进度的基础上继续执行。
  • 当一个线程从 BLOCKED 状态进入到 RUNNABLE 状态时,我们称为一个线程的唤醒,此时线程将获取上次保存的上下文继续完成执行。

通过线程的运行状态以及状态间的相互切换,我们可以了解到,多线程的上下文切换实际上就是由多线程的两个运行状态的互相切换导致的。

那么在线程运行时,线程状态由 RUNNING 转为 BLOCKED 或者由 BLOCKED 转为 RUNNABLE,这又是什么诱发的呢?

我们可以分两种情况来分析,一种是程序本身触发的切换,这种我们称为自发性上下文切换,另一种是由系统或者虚拟机诱发的非自发性上下文切换

  • 自发性上下文切换指线程由 Java 程序调用导致切出,在多线程编程中,执行调用以下方法或关键字,常常就会引发自发性上下文切换。
    • sleep() 、wait() 、yield()、join() 、park()、synchronized 、lock
  • 非自发性上下文切换指线程由于调度器的原因被迫切出。常见的有:线程被分配的时间片用完,虚拟机垃圾回收导致或者执行优先级的问题导致。

这里重点说下“虚拟机垃圾回收为什么会导致上下文切换”。

在 Java 虚拟机中,对象的内存都是由虚拟机中的堆分配的,在程序运行过程中,新的对象将不断被创建,如果旧的对象使用后不进行回收,堆内存将很快被耗尽。Java 虚拟机提供了一种回收机制,对创建后不再使用的对象进行回收,从而保证堆内存的可持续性分配。而这种垃圾回收机制的使用有可能会导致 stop-the-world 事件的发生,这其实就是一种线程暂停行为

3、发现上下文切换

我们总说上下文切换会带来系统开销,那它带来的性能问题是不是真有这么糟糕呢?我们又该怎么去监测到上下文切换?上下文切换到底开销在哪些环节?

线程的上下文切换导致了额外的开销,如果使用 Synchronized 锁关键字,将导致了资源竞争,从而引起了上下文切换,但即使不使用 Synchronized 锁关键字,并发的执行速度也无法超越串联的执行速度,这是因为多线程同样存在着上下文切换。Redis、NodeJS 的设计就很好地体现了单线程串行的优势

在 Linux 系统下,可以使用 Linux 内核提供的 vmstat 命令,来监视 Java 程序运行过程中系统的上下文切换频率, cs 如下图所示:

image-20210630095720153

如果是监视某个应用的上下文切换,就可以使用 pidstat 命令监控指定进程的 Context Switch 上下文切换。

image-20210630095746840

至于系统开销具体发生在切换过程中的哪些具体环节,总结如下:

  • 操作系统保存和恢复上下文;
  • 调度器进行线程调度;
  • 处理器高速缓存重新加载;

总结

上下文切换就是一个工作的线程被另外一个线程暂停,另外一个线程占用了处理器开始执行任务的过程。系统和 Java 程序自发性以及非自发性的调用操作,就会导致上下文切换,从而带来系统开销。

线程越多,系统的运行速度不一定越快。那么我们平时在并发量比较大的情况下,什么时候用单线程,什么时候用多线程呢?

  • 一般在单个逻辑比较简单,而且速度相对来非常快的情况下,我们可以使用单线程。例如,我们前面讲到的 Redis,从内存中快速读取值,不用考虑 I/O 瓶颈带来的阻塞问题;
  • 而在逻辑相对来说很复杂的场景,等待时间相对较长又或者是需要大量计算的场景,我建议使用多线程来提高系统的整体性能。例如,NIO 时期的文件读写操作、图像处理以及大数据分析等。

二、多线程调优(下):如何优化多线程上下文切换?

如果是单个线程,在CPU 调用之后,那么它基本上是不会被调度出去的。如果可运行的线程数远大于 CPU 数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其它线程能够使用 CPU,这就会导致上下文切换。

另外,在多线程中如果使用了竞争锁,当线程由于等待竞争锁而被阻塞时,JVM 通常会将这个锁挂起,并允许它被交换出去。如果频繁地发生阻塞,CPU 密集型的程序就会发生更多的上下文切换。

那么问题来了,我们知道在某些场景下使用多线程是非常必要的,但多线程编程给系统带来了上下文切换,从而增加的性能开销也是实打实存在的。那么我们该如何优化多线程上下文切换呢?

1、竞争锁优化

大多数人在多线程编程中碰到性能问题,第一反应多是想到了锁。

多线程对锁资源的竞争会引起上下文切换,还有锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销也就越大。由此可见,在多线程编程中,锁其实不是性能开销的根源,竞争锁才是

1.减少锁的持有时间

锁的持有时间越长,就意味着有越多的线程在等待该竞争资源释放。如果是 Synchronized 同步锁资源,就不仅是带来线程间的上下文切换,还有可能会增加进程间的上下文切换。

例如,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作。

优化前:

public synchronized void mySyncMethod(){  
    businesscode1();  
    mutextMethod();  
    businesscode2();
}

优化后:

public void mySyncMethod(){  
    businesscode1();  
    synchronized(this)
    {
        mutextMethod();  
    }
    businesscode2();
}
2.降低锁的粒度

同步锁可以保证对象的原子性,我们可以考虑将锁粒度拆分得更小一些,以此避免所有线程对一个锁资源的竞争过于激烈。具体方式有以下两种:

  • 锁分离:读写锁实现了锁分离,也就是说读写锁是由“读锁”和“写锁”两个锁实现的,其规则是可以共享读,但只有一个写。

在读远大于写的多线程场景中,锁分离避免了在高并发读情况下的资源竞争,从而避免了上下文切换。

  • 锁分段:在使用锁来保证集合或者大对象的原子性时,可以考虑将锁对象进一步分解。例如, Java1.8 之前版本的 ConcurrentHashMap 就使用了锁分段。
3. 非阻塞乐观锁替代竞争锁

volatile 关键字的作用是保障可见性及有序性,volatile 的读写操作不会导致上下文切换,因此开销比较小。 但是,volatile 不能保证操作变量的原子性,因为没有锁的排他性。

CAS 是一个原子的 if-then-act 操作,CAS 是一个无锁算法实现,保障了对一个共享变量读写操作的一致性。CAS 操作中有 3 个操作数,内存值 V、旧的预期值 A 和要修改的新值 B,当且仅当 A 和 V 相同时,将 V 修改为 B,否则什么都不做,CAS 算法将不会导致上下文切换。Java 的 Atomic 包就使用了 CAS 算法来更新数据,就不需要额外加锁。

在 JDK1.6 中,JVM 将 Synchronized 同步锁分为了偏向锁、轻量级锁、自旋锁以及重量级锁,优化路径也是按照以上顺序进行。JIT 编译器在动态编译同步块的时候,也会通过锁消除、锁粗化的方式来优化该同步锁。

2、wait/notify 优化

在 Java 中,我们可以通过配合调用 Object 对象的 wait() 方法和 notify() 方法或 notifyAll() 方法来实现线程间的通信。

在线程中调用 wait() 方法,将阻塞等待其它线程的通知(其它线程调用 notify() 方法或 notifyAll() 方法),在线程中调用 notify() 方法或 notifyAll() 方法,将通知其它线程从 wait() 方法处返回。

1.wait/notify 的使用导致了较多的上下文切换

image-20210630102544889

如果有多个消费者线程同时被阻塞,用 notifyAll() 方法,将会唤醒所有阻塞的线程。而某些商品依然没有库存,过早地唤醒这些没有库存的商品的消费线程,可能会导致线程再次进入阻塞状态,从而引起不必要的上下文切换。

2.优化 wait/notify 的使用,减少上下文切换
  • 首先,我们在多个不同消费场景中,可以使用 Object.notify() 替代 Object.notifyAll()。因为 Object.notify() 只会唤醒指定线程,不会过早地唤醒其它未满足需求的阻塞线程,所以可以减少相应的上下文切换。

  • 其次,在生产者执行完 Object.notify() / notifyAll() 唤醒其它线程之后,应该尽快地释放内部锁,这样可以避免被唤醒的线程再次申请相应内部锁的时候需要等待锁的释放。

  • 最后,为了避免长时间等待,我们常会使用 Object.wait (long)设置等待超时时间,但线程无法区分其返回是由于等待超时还是被通知线程唤醒,从而导致线程再次尝试获取锁操作,增加了上下文切换。

推荐使用 Lock 锁结合 Condition 接口替代 Synchronized 内部锁中的 wait /notify,实现等待/通知。这样做不仅可以解决上述的 Object.wait(long) 无法区分的问题,还可以解决线程被过早唤醒的问题。

Condition 接口定义的 await 方法 、signal 方法和 signalAll 方法分别相当于Object.wait()、 Object.notify() 和 Object.notifyAll()。

3.合理地设置线程池大小,避免创建过多线程

线程池的线程数量设置不宜过大,因为一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换。

在有些创建线程池的方法里,线程数量设置不会直接暴露给我们。比如,用 Executors.newCachedThreadPool() 创建的线程池,该线程池会复用其内部空闲的线程来处理新提交的任务,如果没有,再创建新的线程(不受 MAX_VALUE 限制),这样的线程池如果碰到大量且耗时长的任务场景,就会创建非常多的工作线程,从而导致频繁的上下文切换。因此,这类线程池就只适合处理大量且耗时短的非阻塞任务。

4.使用协程实现非阻塞等待

协程是一种比线程更加轻量级的东西,相比于由操作系统内核来管理的进程和线程,协程则完全由程序本身所控制,也就是在用户态执行。协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升。

5.减少 Java 虚拟机的垃圾回收

很多 JVM 垃圾回收器(serial 收集器、ParNew 收集器)在回收旧对象时,会产生内存碎片,从而需要进行内存整理,在这个过程中就需要移动存活的对象。而移动内存对象就意味着这些对象所在的内存地址会发生变化,因此在移动对象前需要暂停线程,在移动完成后需要再次唤醒该线程。因此减少 JVM 垃圾回收的频率可以有效地减少上下文切换。

总结

上下文切换是多线程编程性能消耗的原因之一,而竞争锁、线程间的通信以及过多地创建线程等操作,都会给系统带来上下文切换。除此之外,I/O 阻塞以及 JVM 的垃圾回收也会增加上下文切换。

总的来说,过于频繁的上下文切换会影响系统的性能,所以我们应该避免它。另外,还可以将上下文切换也作为系统的性能参考指标,并将该指标纳入到服务性能监控,防患于未然。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 阿里巴巴是一家业界知名的互联网公司,其在Java性能调优方面有着丰富的经验和独到的见解。由于Java开发语言的独特性,它的性能调优与其他编程语言稍有不同,因此需要专业的指导和实践经验。阿里巴巴发布了一本《阿里巴巴Java性能调优实践》的PDF文档,针对Java程序的性能调优问题进行了详细的阐述和解答。 这本PDF文档包括了Java性能调优的介绍、调优原则与方案、JVM调优、内存泄漏调优、代码质量优化以及性能测试等多个方面的内容。从性能调优的思路、方法、技巧、工具等多个角度进行了讲解,对于Java开发人员来说是一本非常实用的参考资料。 Java性能调优Java程序开发的重要环节之一,通过优化程序性能可以提升应用的质量和效率,减少资源浪费并节省硬件成本。阿里巴巴Java性能调优实践文档提供了丰富的实例和方法,使开发者更好地理解Java性能调优的概念和实践,提高编程和性能优化的技能。对Java开发人员和相关领域的从业者来说,这是一本难得的学习资料,建议大家下载学习。 ### 回答2: 阿里巴巴Java性能调优PDF下载提供了丰富的知识和经验,有助于提高Java应用程序的性能和稳定性。这份PDF文档介绍了如何诊断Java应用程序的性能问题,并提出了一些有效的解决方法。其中包括对JVM垃圾回收的优化、线程管理、代码优化等方面的建议。此外,文档还介绍了一些常用的性能分析工具,如JProfiler、JConsole等,以及如何利用这些工具来定位性能瓶颈。对于想要了解如何提升Java应用程序性能的开发者来说,这份PDF文档是一个很好的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值