线程的上下文切换
上下文切换
1.对于CPU而言,在一个时刻只能运因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器行一个线程,当一个线程的时间片用完,或者
2.当CPU结束运行一个线程,转去执行另外一个线程,这个过程就叫做线程上下文切换
上下文
1.在发生切换的时候,当前线程的任务可能并没有执行完毕。所以在切换时需要保存线程切换前的运行状态,以便下一次,可以接着切换之前的状态继续执行后续的任务
2.切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文
比如一个线程A正在读取一个文件的内容,正读到文件的一半,线程A的时间片结束,此时需要暂停线程A,CPU转去执行线程;当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取
上下文的内容
线程上下文切换过程中会涉及程序计数器(PCB,位于内存之中)、CPU寄存器∶
- 寄存器的存储内容∶CPU寄存器负责存储已经、正在和将要执行的任务
- 程序计数器存储的指令内容∶程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置
CPU寄存器
在线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少(也就是直接读取中间的结果,而不是从头开始读);中间结果会放在CPU的寄存器中
程序计数器
- 线程在进行切换时候,需要知道在这之前当前线程已经执行到哪条指令了,这些指令信息需要依靠程序计数器来保存
- 程序计数器是一块较小的内存空间,它保存了当前线程下一条需要执行的字节码指令的位置
- 每条线程都需要有一个独立的程序计数器,线程之间的程序计数器互不影响,寄存器和CPU是不同的
带来的系统开销
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源;比如运行的QQ和微信等都是运行在用户态中的
用户态-内核态
- 在执行用户自己的代码时,称其处于用户态,此时处理器特权级最低,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。(比如QQ,微信等等)
- 当因为系统调用陷入内核代码中执行时(阻塞和唤醒),处于内核态,此时处理器处于特权级最高
- 如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,此时需要从用户态切换到内核态。这些系统调用会调用内核的代码,在执行完后又会切换回用户态
- 用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
上下文切换的两种方式:自发性上下文切换和非自发性地上下文切换
自发性上下文切换:主动进入阻塞状态
- sleep
- wait ;等待一些情形的发生和等待锁
- yield :主动地释放自己的CPU资源
- join等待其他线程完成,然后再继续自己的任务
- park
- synchronized
- lock
非自发性上下文切换:被动进入阻塞状态
- 线程被分配的时间片用完
- JVM垃圾回收(STW Stop the world:full GC、线程暂停)
- 线程执行优先级
-
如何优化?-从减少线程上下文切换的角度出发
减少锁的竞争
- 多线程对锁资源的竞争,如果失败由于进入阻塞状态,将会引起上下文切换(锁本身不是带来性能开销的本质原因,锁竞争才是)
- 锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销就越大
- 在多线程编程中,锁本身不是性能开销的根源,锁竞争才是性能开销的根源
- 锁优化归根到底是减少竞争
-
减少锁的持有时间
- 锁的持有时间越长,意味着越多的线程会被阻塞,在等待该竞争锁释放
- 优化方法∶将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作
-
减少锁的粒度-锁分离
- 读写锁实现了锁分离,由读锁和写锁两个锁实现,可以共享读,但只有一个写
- 读写锁在多线程读写时,读读不互斥,读写互斥,写写互斥
- 传统的独占锁在多线程读写时,读读互斥,读写互斥,写写互斥
- 在读远大于写的多线程场景中,锁分离避免了高并发读情况下的资源竞争,从而避免了上下文切换
-
减少锁的粒度-锁分段
- 在使用锁来保证集合或者大对象的原子性时,可以将锁对象进一步分解
- Java 1.8之前的ConcurrentHashMap就是用了锁分段