线程的上下文切换
- 什么是上下文切换
- cpu时间片和线程
- 上下文切换Context Switch
- 上下文的内容
- 上下文切换的原因
- 上下文切换带来的开销
- 如何减少上下文的开销
- 竞争锁优化
- 减少锁的持有时间
- 减少锁的粒度
- 非阻塞乐观锁代替竞争锁
- synchronized锁优化
- 合理的线程池大小
- 协程:非阻塞等待
- 减少GC频率
什么是上下文切换
cpu时间片和线程
在单处理器时期,操作系统就能处理多线程并发任务,处理器给每个线程分配CPU时间片,线程在CPU时间片内执行任务。CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
时间片决定了一个线程可以连续占用处理器运行的时长
当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器
上下文切换Context Switch
一个线程被暂停剥夺使用权,另一个线程被选中开始或者继续运行的过程。
切出:一个线程被剥夺处理器的使用权而被暂停运行
切入:一个线程被选中占用处理器开始运行或者继续运行
切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文
上下文的内容
寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务
程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置
当CPU数量远远不止1个的情况下,操作系统将CPU轮流分配给线程任务,此时的上下文切换会变得更加频繁
并且存在跨CPU的上下文切换,更加昂贵
上下文切换的原因
1.在操作系统中,上下文切换的类型可以分为进程间的上下文切换和线程间的上下文切换
2.线程状态:NEW、RUNNABLE、RUNNING、BLOCKED、DEAD
Java线程状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
3.线程上下文切换:RUNNING -> BLOCKED -> RUNNABLE -> 被调度器选中执行
a.一个线程从RUNNING状态转为BLOCKED状态,称为一个线程的暂停
b.线程暂停被切出后,操作系统会保存相应的上下文
c.以便该线程再次进入RUNNABLE状态时能够在之前执行进度的基础上继续执行
d,一个线程从BLOCKED状态进入RUNNABLE状态,称为一个线程的唤醒
e.此时线程将获取上次保存的上下文继续执行
4.诱因:程序本身触发的自发性上下文切换、系统或虚拟机触发的非自发性上下文切换
a.自发性上下文切换
b.sleep、wait、yield、join、park、synchronized、lock
c.非自发性上下文切换
d.线程被分配的时间片用完、JVM垃圾回收(STW、线程暂停)、线程执行优先级
上下文切换带来的开销
1.操作系统保存和恢复上下文
2.调度器进行线程调度
3.处理器高速缓存重新加载
4.可能导致整个高速缓存区被冲刷,从而带来时间开销
如何减少上下文的开销
竞争锁优化
1.多线程对锁资源的竞争会引起上下文切换,锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销就越大
2.在多线程编程中,锁本身不是性能开销的根源,锁竞争才是性能开销的根源
3.锁优化归根到底是减少竞争
减少锁的持有时间
1.锁的持有时间越长,意味着越多的线程在等待该竞争锁释放
2.如果是synchronized同步锁资源,不仅带来了线程间的上下文切换,还有可能会带来进程间的上下文切换
3.优化方法:将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作
减少锁的粒度
锁分离
1.读写锁实现了锁分离,由读锁和写锁两个锁实现,可以共享读,但只有一个写
1.1.读写锁在多线程读写时,读读不互斥,读写互斥,写写互斥
1.2.传统的独占锁在多线程读写时,读读互斥,读写互斥,写写互斥
2.在读远大于写的多线程场景中,锁分离避免了高并发读情况下的资源竞争,从而避免了上下文切换
锁分段
1.在使用锁来保证集合或者大对象的原子性时,可以将锁对象进一步分解
2.Java 1.8之前的ConcurrentHashMap就是用了锁分段
非阻塞乐观锁代替竞争锁
volatile
1.volatile关键字的作用是保证可见性和有序性,volatile的读写操作不会导致上下文切换,开销较小
2.由于volatile关键字没有锁的排它性,因此不能保证操作变量的原子性
CAS
1.CAS是一个原子的if-then-act操作
2.CAS是一个无锁算法实现,保障了对一个共享变量读写操作的一致性
3.CAS不会导致上下文切换,Java的Atomic包就使用了CAS算法来更新数据,而不需要额外加锁
synchronized锁优化
1.在JDK 1.6中,JVM将synchronized同步锁分为偏向锁、轻量级锁、自旋锁、重量级锁
2.JIT编译器在动态编译同步代码块时,也会通过锁消除、锁粗化的方式来优化synchronized同步锁
合理的线程池大小
1.线程池的线程数量不宜过大
2.一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换
协程:非阻塞等待
1.协程比线程更加轻量,相比于由操作系统内核管理的进程和线程,协程完全由程序本身所控制,即在用户态执行
2.协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升
减少GC频率
1.GC会导致上下文切换
2.很多垃圾回收器在回收旧对象时会产生内存碎片,从而需要进行内存整理,该过程需要移动存活的对象,而移动存活的对象意味着这些对象的内存地址会发生改变,因此在移动对象之前需要暂停线程,完成后再唤醒线程
3.因此减少GC的频率能够有效的减少上下文切换