Java性能调优实战
刘超
金山软件西山居技术经理
立即订阅
7535 人已学习
课程目录
已完结 48 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 怎样才能做好性能调优?
免费
模块一 · 概述 (2讲)
01 | 如何制定性能调优标准?
02 | 如何制定性能调优策略?
模块二 · Java编程性能调优 (10讲)
03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据
04 | 慎重使用正则表达式
05 | ArrayList还是LinkedList?使用不当性能差千倍
加餐 | 推荐几款常用的性能测试工具
06 | Stream如何提高遍历集合效率?
07 | 深入浅出HashMap的设计与优化
08 | 网络通信优化之I/O模型:如何解决高并发下I/O瓶颈?
09 | 网络通信优化之序列化:避免使用Java序列化
10 | 网络通信优化之通信协议:如何优化RPC网络通信?
11 | 答疑课堂:深入了解NIO的优化实现原理
模块三 · 多线程性能调优 (10讲)
12 | 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法
13 | 多线程之锁优化(中):深入了解Lock同步锁的优化方法
14 | 多线程之锁优化(下):使用乐观锁优化并行操作
15 | 多线程调优(上):哪些操作导致了上下文切换?
16 | 多线程调优(下):如何优化多线程上下文切换?
17 | 并发容器的使用:识别不同场景下最优容器
18 | 如何设置线程池大小?
19 | 如何用协程来优化多线程业务?
20 | 答疑课堂:模块三热点问题解答
加餐 | 什么是数据的强、弱一致性?
模块四 · JVM性能监测及调优 (6讲)
21 | 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型
22 | 深入JVM即时编译器JIT,优化Java编译
23 | 如何优化垃圾回收机制?
24 | 如何优化JVM内存分配?
25 | 内存持续上升,我该如何排查问题?
26 | 答疑课堂:模块四热点问题解答
模块五 · 设计模式调优 (6讲)
27 | 单例模式:如何创建单一对象优化系统性能?
28 | 原型模式与享元模式:提升系统性能的利器
29 | 如何使用设计模式优化并发编程?
30 | 生产者消费者模式:电商库存设计优化
31 | 装饰器模式:如何优化电商系统中复杂的商品价格策略?
32 | 答疑课堂:模块五思考题集锦
模块六 · 数据库性能调优 (8讲)
33 | MySQL调优之SQL语句:如何写出高性能SQL语句?
34 | MySQL调优之事务:高并发场景下的数据库事务调优
35 | MySQL调优之索引:索引的失效与优化
36 | 记一次线上SQL死锁事故:如何避免死锁?
37 | 什么时候需要分表分库?
38 | 电商系统表设计优化案例分析
39 | 数据库参数设置优化,失之毫厘差之千里
40 | 答疑课堂:MySQL中InnoDB的知识点串讲
模块七 · 实战演练场 (4讲)
41 | 如何设计更优的分布式锁?
42 | 电商系统的分布式事务调优
43 | 如何使用缓存优化系统性能?
44 | 记一次双十一抢购性能瓶颈调优
结束语 (1讲)
结束语 | 栉风沐雨,砥砺前行!
Java性能调优实战
登录|注册

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

刘超 2019-06-25
你好,我是刘超。
通过上一讲的讲解,相信你对上下文切换已经有了一定的了解了。如果是单个线程,在 CPU 调用之后,那么它基本上是不会被调度出去的。如果可运行的线程数远大于 CPU 数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其它线程能够使用 CPU ,这就会导致上下文切换。
还有,在多线程中如果使用了竞争锁,当线程由于等待竞争锁而被阻塞时,JVM 通常会将这个锁挂起,并允许它被交换出去。如果频繁地发生阻塞,CPU 密集型的程序就会发生更多的上下文切换。
那么问题来了,我们知道在某些场景下使用多线程是非常必要的,但多线程编程给系统带来了上下文切换,从而增加的性能开销也是实打实存在的。那么我们该如何优化多线程上下文切换呢?这就是我今天要和你分享的话题,我将重点介绍几种常见的优化方法。

竞争锁优化

大多数人在多线程编程中碰到性能问题,第一反应多是想到了锁。
多线程对锁资源的竞争会引起上下文切换,还有锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销也就越大。由此可见,在多线程编程中,锁其实不是性能开销的根源,竞争锁才是。
第 11~13 讲中我曾集中讲过锁优化,我们知道锁的优化归根结底就是减少竞争。这讲中我们就再来总结下锁优化的一些方式。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java性能调优实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(26)

  • Geek_1f1a07
    Zed说的不对,首先,所有的锁,无论synchronize还是lock,如果发生竞争条件,都可能造成上下文切换,优化锁的目的是为了尽量降低发生锁竞争的概率,synchronize做的优化都是把竞争的可能消灭在前期的偏向锁,轻量级锁,把会造成上下文切换的“脏活”留在最后。lock的乐观锁大体思路也是一样的,不到万不得已,不会轻易调用park方法。但是本质上java目前都是利用内核线程,所以都会有上下文切换。除非使用协程的技术,这个以前有green thread,后来不用了,期待老师后面对协程的讲解。

    作者回复: 回答很好,赞一个。

    2019-06-25
    17
  • QQ怪
    我觉得有些人建议使用notifyall的原因是使用notify需要有十足的把握去确认哪条线程需要唤醒,因为一不留神就容易搞错,为了优化而优化最后事倍功半,所以大家才会使用notifyall一劳永逸,我其实挺认同老师的观点,老师,全部唤醒会导致更多的上下文切换,是否要优化这点,我觉得还是得看个人了吧😂

    作者回复: notify()可以结合wait(long)方法使用,解决某些没有通知的线程被通知不到的问题

    2019-06-26
    2
    6
  • WL
    老师请问一下在一段程序中除了工作线程之外还有很多守护线程, 这些线程加起来的数量必然比cup的数量会多很多, 那么为什么创建线程池的时候要参考CPU的数量呢, 为什么不把守护线程也考虑进去呢?
    2019-06-25
    5
  • Zed
    回答趙衍同学

    如你所说,synchronized主要是因为有用户态和内核态的交互所以能到进程级别。

    而Lock是通过AQS的state状态来判断是否持有锁,整个过程都是在用户态或者说纯java实现。

    最后lock.await()也是把当前线程放到当前条件变量的等待队列中并让出cpu。顺便提下,lock支持多条件变量。

    作者回复: 回答很好。线程进入阻塞,两者都会发生进程上下文切换。Synchronized中阻塞线程无论何时去获取锁,都需要进入到内核态,而AQS中,阻塞线程再次获取锁时,是通过state以及CAS操作判断,只有没有竞争成功时,才会再次被挂起,这样可以尽量减少上下文切换。

    2019-06-25
    4
  • K
    老师好,我有个特别简单的小问题不太明白。既然用了vector,为什么还要用synchronize锁起来啊,vector本身不就是线程安全的?谢谢老师回答。

    作者回复: 这里的vector是一个对象锁,锁的是一个代码块,并不是保证vector的线程安全。

    2019-07-22
    3
  • 疯狂咸鱼
    volitile的读写不会导致上下文切换,操作系统层面怎么理解呢

    作者回复: volatile主要是用来保证共享变量额可见性,以及防止指令重排序,保证执行的有序性。

    通过生成.class文件之后,反编译文件我们可以看到通过volatile修饰的共享变量,在写入操作的时候会多一个Lock前缀这样的指令,当操作系统执行时会由于这个指令,将当前处理器缓存的数据写回系统内存中,并通知其他处理器中的缓存失效。

    所以volatile不会带来线程的挂起操作,不会导致上下文切换。

    2019-09-21
    2
  • 💪😊
    多个软件共同运行也有可能导致上下文切换,有些软件考虑使用绑定固定cpu核方式运行
    2019-06-25
    2
  • 皮皮
    老师您好,一直有个疑问想请教,就是JDK1.5引入的lock锁底层实现也是调用了lockhelper的park和unpark方法,这个是否也涉及到系统的上下文切换,用户态和内核态的切换?

    作者回复: 是的

    2019-06-25
    1
  • 趙衍
    老师好!在synchronized中,“挂起”这个动作是由JVM来实现的,获取不到锁的线程会被迫让出CPU,由于synchronized是基于操作系统的mutex机制,所以会产生进程的上下文切换。我想请问老师,在JDK的Lock中,或者AQS中,线程“挂起”这个动作又是怎么实现的呢?为什么不会产生进程级别的上下文切换呢?

    作者回复: AQS挂起是通过LockSupport中的park进入阻塞状态,这个过程也是存在进程上下文切换的。但被阻塞的线程再次获取锁时,不会产生进程上下文切换,而synchronized阻塞的线程每次获取锁资源都要通过系统调用内核来完成,这样就比AQS阻塞的线程更消耗系统资源了。

    2019-06-25
    1
  • vivi
    请问,那事物的开启和锁的持有时间该如何权衡?例如我方法里设计到三个不同表的插入更新操作,其中一个是库存表,是该把锁加在事物外面还是在事物中进行库存表的加锁操作

    作者回复: 在事务内加,其实这种情况使用乐观锁来锁库存会性能好一些

    2019-09-30
  • Young
    请问老师,线程wait区分是由于等待超时重新运行为什么不需要再去重新获取锁呢,我的理解是,wait后锁被释放了,那线程重新恢复运行后无论什么情况下都应该先去获取锁

    作者回复: wait是在锁代码块里面,所以一旦超时,则会跳出该同步锁代码块

    2019-09-19
  • godtrue
    这节很不错,不过疑问还是有的
    感觉老师没有完全讲清楚进程的上下文切换和线程的上下文切换?另外,老师对于什么是进程?什么是线程?他们之间的区别与联系也是没有讲的比较细致?这两个概念非常重要,不过能通俗易懂的讲明白的不多。另外,不管进程还是线程我认为若想被CPU执行,少不了要进入内核态。进进出出比较费劲但又不得不进,那就减少进进出出的次数,少进为妙,少进的方法就是少触发那些进进出出的条件。比如:减少锁持有时间,减少锁粒度,少触发锁竞争,减少FULL GC,减少IO阻塞,创建合适的线程数等等。

    作者回复: 进程的上下文切换指的是用户态和内核态的相互切换,后续补上进程的上下文切换。

    2019-09-09
    1
  • 风轻扬
    老师,如果将文中的例子的wait,notify,notifyAll,全部换成Condition的await,signal和signalAll方法。我试了一下,只要把synchronized换成lock锁就可以实现生产10个数据,然后消费10个数据。有两个问题请教您,望老师不吝指教
    1 .就这个功能来说,直接一对一替换api,有什么需要注意的地方吗?我第一次使用Condition
    2 .如果生产者await等待超时,再次获取锁的行为,需要怎么做呢?
    2019-08-27
  • devin.ou
    老师,能给出wait/notify优发后的代码么

    作者回复: 你好 devin,在第29讲中讲到了具体的优化。

    2019-07-21
  • Demon.Lee
    1. ‘’在 JDK1.6 中,JVM 将 Synchronized 同步锁分为了偏向锁、轻量级锁、偏向锁以及重量级锁,优化路径也是按照以上顺序进行。‘’
     这句话里面有两个偏向锁,后一个是不是“自旋锁”呀

    2. 老师只说了减少垃圾回收频率可以减少上下文切换,没说如何减少回收频率。感觉不是个好问题,算了,我还是先去Google下找答案吧。

    作者回复: 你好DemonLee同学,第一句是偏向锁、轻量级锁、自旋锁以及重量级锁;
    第22讲中详细讲解了优化垃圾回收的内容,可以跳过去先了解下。

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

    老师,这个只是JDK1.6的优化,还是1.6以后都是这么优化的?

    作者回复: 包括了JDK1.6

    2019-07-04
  • 奇奇
    不同时执行remive 但是也会进去执行remove(0)也是不符合语义的

    作者回复: 是的,这个循环应该放到锁里面。已修正,谢谢提醒。

    2019-07-03
  • 奇奇
    代码写错了
    while(pool.isEmpty())不能放在同步代码块的外面
    假设此时pool不为空容量为1,此时10个线程的pool.isEmpty都为false,此时全部跳出循环。
    全部执行pool.remove(0) 错误

    编辑回复: 同学你好!后面有个锁,不会同时进去remove。如有疑问,可继续留言。

    2019-07-02
  • WL
    老师请问一下, JVM在操作系统层面是一个进程还是多个进程, 如果是一个进程的话, 那synchronize和park()方法发生的是进程级别的状态切换的话是指操作系统不运行JVM了吗?

    作者回复: 一个JVM在操作系统中只有一个进程,这里指的是进程中的某个运行的线程停止使用CPU,切换到内核获取CPU运行,而不是说停止JVM,然后运行内核。这里的切换是用户态使用CPU切换到了内核态使用CPU。

    2019-06-27
  • 梁中华
    原文:“而移动内存对象就意味着这些对象所在的内存地址会发生变化,因此在移动对象前需要暂停线程,在移动完成后需要再次唤醒该线程”。 这句话是不是不太严密?每次ygc都会导致年轻代内存地址变化,这也会导致暂停线程吗?如果是的话,那线程切换也太频繁了,似乎和事实不符啊。

    作者回复: 年轻代是部分对象复制过程,是不会存在stop the world的发生。如果存在对象移动,使用对象的线程是会被挂起的,这个过程存在上下文切换。

    2019-06-26
收起评论
26
返回
顶部