16 | 多线程调优(下):如何优化多线程上下文切换?
16 | 多线程调优(下):如何优化多线程上下文切换?
讲述:李良
时长11:27大小10.48M
竞争锁优化
1. 减少锁的持有时间
2. 降低锁的粒度
3. 非阻塞乐观锁替代竞争锁
wait/notify 优化
wait/notify 的使用导致了较多的上下文切换
优化 wait/notify 的使用,减少上下文切换
合理地设置线程池大小,避免创建过多线程
使用协程实现非阻塞等待
减少 Java 虚拟机的垃圾回收
总结
思考题
赞 7
提建议
精选留言(38)
- Geek_1f1a072019-06-25Zed说的不对,首先,所有的锁,无论synchronize还是lock,如果发生竞争条件,都可能造成上下文切换,优化锁的目的是为了尽量降低发生锁竞争的概率,synchronize做的优化都是把竞争的可能消灭在前期的偏向锁,轻量级锁,把会造成上下文切换的“脏活”留在最后。lock的乐观锁大体思路也是一样的,不到万不得已,不会轻易调用park方法。但是本质上java目前都是利用内核线程,所以都会有上下文切换。除非使用协程的技术,这个以前有green thread,后来不用了,期待老师后面对协程的讲解。展开
作者回复: 回答很好,赞一个。
55 - 尔冬橙2019-09-21volitile的读写不会导致上下文切换,操作系统层面怎么理解呢
作者回复: volatile主要是用来保证共享变量额可见性,以及防止指令重排序,保证执行的有序性。 通过生成.class文件之后,反编译文件我们可以看到通过volatile修饰的共享变量,在写入操作的时候会多一个Lock前缀这样的指令,当操作系统执行时会由于这个指令,将当前处理器缓存的数据写回系统内存中,并通知其他处理器中的缓存失效。 所以volatile不会带来线程的挂起操作,不会导致上下文切换。
18 - Zed2019-06-25回答趙衍同学 如你所说,synchronized主要是因为有用户态和内核态的交互所以能到进程级别。 而Lock是通过AQS的state状态来判断是否持有锁,整个过程都是在用户态或者说纯java实现。 最后lock.await()也是把当前线程放到当前条件变量的等待队列中并让出cpu。顺便提下,lock支持多条件变量。展开
作者回复: 回答很好。线程进入阻塞,两者都会发生进程上下文切换。Synchronized中阻塞线程无论何时去获取锁,都需要进入到内核态,而AQS中,阻塞线程再次获取锁时,是通过state以及CAS操作判断,只有没有竞争成功时,才会再次被挂起,这样可以尽量减少上下文切换。
15 - QQ怪2019-06-26我觉得有些人建议使用notifyall的原因是使用notify需要有十足的把握去确认哪条线程需要唤醒,因为一不留神就容易搞错,为了优化而优化最后事倍功半,所以大家才会使用notifyall一劳永逸,我其实挺认同老师的观点,老师,全部唤醒会导致更多的上下文切换,是否要优化这点,我觉得还是得看个人了吧😂
作者回复: notify()可以结合wait(long)方法使用,解决某些没有通知的线程被通知不到的问题
共 3 条评论13 - WL2019-06-25老师请问一下在一段程序中除了工作线程之外还有很多守护线程, 这些线程加起来的数量必然比cup的数量会多很多, 那么为什么创建线程池的时候要参考CPU的数量呢, 为什么不把守护线程也考虑进去呢?12
- 你好旅行者2019-06-25老师好!在synchronized中,“挂起”这个动作是由JVM来实现的,获取不到锁的线程会被迫让出CPU,由于synchronized是基于操作系统的mutex机制,所以会产生进程的上下文切换。我想请问老师,在JDK的Lock中,或者AQS中,线程“挂起”这个动作又是怎么实现的呢?为什么不会产生进程级别的上下文切换呢?
作者回复: AQS挂起是通过LockSupport中的park进入阻塞状态,这个过程也是存在进程上下文切换的。但被阻塞的线程再次获取锁时,不会产生进程上下文切换,而synchronized阻塞的线程每次获取锁资源都要通过系统调用内核来完成,这样就比AQS阻塞的线程更消耗系统资源了。
共 2 条评论10 - td9011052019-12-20老师,是不是使用Lock锁机制不会有用户态和内核态的切换?还是Lock本身锁机制是不涉及用户态到内核态的切换的,只是在未获取锁的时候需要使用内核态的方法比如park方法进行线程的挂起?
作者回复: 一样会有,相对同步锁来说,只是减少了用户态和内核态的切换。Lock锁被阻塞的线程再次获取锁时,不会产生进程上下文切换,而synchronized阻塞的线程每次获取锁资源都要通过系统调用内核来完成。
5 - K2019-07-22老师好,我有个特别简单的小问题不太明白。既然用了vector,为什么还要用synchronize锁起来啊,vector本身不就是线程安全的?谢谢老师回答。
作者回复: 这里的vector是一个对象锁,锁的是一个代码块,并不是保证vector的线程安全。
共 2 条评论5 - WL2019-06-27老师请问一下, JVM在操作系统层面是一个进程还是多个进程, 如果是一个进程的话, 那synchronize和park()方法发生的是进程级别的状态切换的话是指操作系统不运行JVM了吗?
作者回复: 一个JVM在操作系统中只有一个进程,这里指的是进程中的某个运行的线程停止使用CPU,切换到内核获取CPU运行,而不是说停止JVM,然后运行内核。这里的切换是用户态使用CPU切换到了内核态使用CPU。
5 - 皮皮2019-06-25老师您好,一直有个疑问想请教,就是JDK1.5引入的lock锁底层实现也是调用了lockhelper的park和unpark方法,这个是否也涉及到系统的上下文切换,用户态和内核态的切换?
作者回复: 是的
4 - 奇奇2019-07-02代码写错了 while(pool.isEmpty())不能放在同步代码块的外面 假设此时pool不为空容量为1,此时10个线程的pool.isEmpty都为false,此时全部跳出循环。 全部执行pool.remove(0) 错误
编辑回复: 同学你好!后面有个锁,不会同时进去remove。如有疑问,可继续留言。
3 - ヾ(◍°∇°◍)ノ゙2019-06-25多个软件共同运行也有可能导致上下文切换,有些软件考虑使用绑定固定cpu核方式运行3
- 梁中华2019-06-26原文:“而移动内存对象就意味着这些对象所在的内存地址会发生变化,因此在移动对象前需要暂停线程,在移动完成后需要再次唤醒该线程”。 这句话是不是不太严密?每次ygc都会导致年轻代内存地址变化,这也会导致暂停线程吗?如果是的话,那线程切换也太频繁了,似乎和事实不符啊。
作者回复: 年轻代是部分对象复制过程,是不会存在stop the world的发生。如果存在对象移动,使用对象的线程是会被挂起的,这个过程存在上下文切换。
共 2 条评论2 - Young2019-09-19请问老师,线程wait区分是由于等待超时重新运行为什么不需要再去重新获取锁呢,我的理解是,wait后锁被释放了,那线程重新恢复运行后无论什么情况下都应该先去获取锁
作者回复: wait是在锁代码块里面,所以一旦超时,则会跳出该同步锁代码块
1 - 钱2019-09-09这节很不错,不过疑问还是有的 感觉老师没有完全讲清楚进程的上下文切换和线程的上下文切换?另外,老师对于什么是进程?什么是线程?他们之间的区别与联系也是没有讲的比较细致?这两个概念非常重要,不过能通俗易懂的讲明白的不多。另外,不管进程还是线程我认为若想被CPU执行,少不了要进入内核态。进进出出比较费劲但又不得不进,那就减少进进出出的次数,少进为妙,少进的方法就是少触发那些进进出出的条件。比如:减少锁持有时间,减少锁粒度,少触发锁竞争,减少FULL GC,减少IO阻塞,创建合适的线程数等等。展开
作者回复: 进程的上下文切换指的是用户态和内核态的相互切换,后续补上进程的上下文切换。
共 2 条评论1 - undifined2019-06-26老师,在并发编程那个专栏第 6 讲中老师提到:notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程;即使使用 notifyAll(),也只有一个线程能够进入临界区;但是 notify() 的风险在于可能导致某些线程永远不会被通知到;所以除非有特殊考虑,否则尽量使用notifyAll() 如果现在又考虑到锁,应该怎么做选择展开
作者回复: notify()可以结合wait(long)方法使用,解决某些没有通知的线程被通知不到的问题
1 - 欧星星2019-06-25sync在使用重量级锁的时候会有上下文切换,lock由于内部是Java实现,锁的等待是基于park来的,所以在lock中只会有线程切换带来的CPU上下文切换,没有锁竞争的上下文切换,比sync少一次CPU上下文切换1
- Jxin2019-06-25g1并行垃圾回收。不一定会上下文切换吧。至于上下文切换这个,java还有信号量的实现。
作者回复: g1只是减少,不能避免哦。
1 - 杯莫停2022-07-08notify和notifyall各有各的好处,用notify有可能造成线程姬饥饿。
- keep_it_real2022-06-21频繁IO就是诱因,频繁的数据库操作,各种拷贝,都是大量的上线文切换。