赞
踩
讲上下文切换前我们先了解下什么是内核空间和用户空间,程序运行在内核空间的状态称为内核态,运行在用户空间的状态称为用户态,用户态和内核态是操作系统的两种运行状态,划分为这两种空间状态主要是为了对应用程序的访问能力进行限制,防止应用程序随意进行一些危险的操作导致系统崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成:
- ① 内核态:内核态运行的程序可以不受限制地访问计算机的任何数据和资源,比如外围设备网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且所占用的 CPU 不会发生抢占情况。
- ② 用户态:用户态运行的程序只能受限地访问内存空间,只能直接读取用户程序的数据,不允许访问外围设备网卡、硬盘等,用户态下所占有的 CPU 会被其他程序抢占,不允许独占。
如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成,从用户空间切换到内核空间,直到完成相关的操作后再切合用户空间,两种状态间的切换,就涉及到 CPU 的上下文切换
系统调用过程中是如何发生 CPU 上下文的切换的呢?我们再了解两个概念:
- ① CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。
- ② 程序计数器,则是用来存储 CPU 正在执行的指令位置以及即将执行的下一条指令位置。
CPU 寄存器和程序计数器都是 CPU 在运行任何任务时必须的依赖环境,因此也被叫做 CPU 上下文。而 CPU 上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
回到系统调用的问题上,为了切换到内核态,需要先保存 CPU 寄存器中用户态的指令位置,然后更新 CPU 寄存器为内核态指令的新位置,最后跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
系统调用的上下文切换跟我们常说的进程上下文切换是不一样的,进程上下文切换是指从一个进程切换到另一个进程运行,而系统调用过程中一直是同一个进程在运行,不会切换进程。
那么两者的具体区别呢?首先,进程都是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文切换不仅包括内核堆栈、寄存器等内核空间的状态,还包括了虚拟内存、栈、全局变量等用户空间的资源。因此进程的上下文切换就比系统调用导致的上下文切换多了一个步骤,在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来,等加载了下一个进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
线程与进程最大的区别在于,进程是资源分配的基本单位,而线程是调度的基本单位,内核中的任务调度,实际上的调度对象是线程;同一个进程中的所有线程共享进程的虚拟内存、全局变量等资源。
在处理多线程并发任务时,处理器会给每个线程分配CPU时间片,线程在各自分配的时间片内占用处理器并执行任务,当线程的时间片用完了,或者自身原因被迫暂停运行的时候,就会有另外一个线程来占用这个处理器,这种一个线程让出处理器使用权,另外一个线程获取处理器使用权的过程就叫做上下文切换。
一个线程让出CPU处理器使用权,就是“切出”;另外一个线程获取CPU处理器使用权,就是“切入”,在这个切入切出的过程中,操作系统会保存和恢复相关的进度信息,而这个进度信息就是我们常说的“上下文”,也就是我们上文提到的 CPU寄存器以及程序计数器。
这么一来,线程的上下文切换就可以分为两种情况:
所以虽然同为线程的上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这正是多线程代替多进程的一个优势。但在并发编程中,并不是线程越多就效率越高,线程数太少可能导致资源不能充分利用,线程数太多可能导致竞争资源激烈,导致上下文切换频繁造成系统的额外开销,因为上下文的保存和恢复过程是有成本的,需要内核在 CPU 上完成,每次切换都需要几十纳秒到数微秒的 CPU 时间,在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上。
所以在单个逻辑比较简单而且速度相对来说非常快的情况下,我们推荐是使用单线程。如果逻辑非常复杂,或者需要进行大量的计算的地方,我们建议使用多线程来提高系统的性能。
对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。每个时间片的大小一般为几十毫秒,所以在一秒钟就可能发生几十上百次的线程相互切换,给我们的感觉就是同时进行的
Java 中多线程的上下文切换就是线程两个运行状态(Java 线程的状态介绍见文章的第二部分)的相互切换导致的,在切换时操作系统保存的上下文信息,当线程从 BLOCKED 状态进入到 RUNNABLE 时,也就是线程的唤醒,此时线程将获取上次保存的上下文信息,接着之前的进度继续执行。
在 Java 中有两种情况会导致线程上下文切换:一种是自发性上下文切换,也就是程序本身触发的切换;另一种是非自发性上下文切换,也就是系统或者虚拟机导致的上下文切换。
(1)自发性上下文是线程由 Java 程序调用导致切出,一般是在编码的时候,调用以下几个方法或关键字:
sleep()、wait()、yield()、join()、park()、synchronized、lock
(2)非自发的上下文切换常见的有:线程被分配的时间片用完,虚拟机垃圾回收导致,或者执行优先级的问题导致
文章看到这里,比较细心的读者可能会有一个疑问:Java线程与操作系统线程的关系?对这个问题比较感兴趣的读者可以阅读这篇文章:https://blog.csdn.net/a745233700/article/details/109410376
上面介绍了几种不同的上下文切换,那么上下文切换会造成什么问题呢?大致如下:
在 JDK 的 java.lang.Thread.State 源码中定义了6个状态,在某一时刻,一个线程只能处于一种状态:
(1)New:实现 Runnable 接口和继承 Thread 类可以得到一个线程类,当线程实例被 new 创建后,就进入了 NEW 状态,但是此时线程还没有开始执行,
(2)RUNNABLE:标识线程已经准备就绪,等待CPU调度,此时还是还没真正执行。转换成该状态的条件:
① 当调用线程的 start() 方法时,线程也不一定会马上执行,因为 Java 线程是映射到操作系统的线程执行的,还需要等CPU调度,但此时该线程的状态已经为 RUNNABLE
② 当前线程时间片用完
③ Thread.yield():当前线程调用 yield() 方法,放弃所获取的 CPU 时间片,由运行状态变会可运行状态,让同优先级的线程执行,但不保证一定能达到让步的目的,因为让步的线程有可能被线程调度程序再次选中。
④ join() 的线程结束:在当前线程里调用线程A的 join() 方法,当前线程阻塞但不释放对象锁,直到线程A执行完毕 或者 millis 时间到,当前线程进入可运行状态。
⑤ Thread.sleep(long millis):当前线程调用 sleep() 方法,当前线程进入阻塞但不释放对象锁,millis 后线程自动苏醒进入可运行态。
⑥ 锁池里的线程拿到对象锁后,进入可运行状态
(3)WAIT 等待:等待状态,处于等待状态的线程正在等待另一个线程执行特定操作。例如:
- public enum State {
- /**
- * Thread state for a thread which has not yet started.
- */
- NEW,
-
- /**
- * Thread state for a runnable thread. A thread in the runnable
- * state is executing in the Java virtual machine but it may
- * be waiting for other resources from the operating system
- * such as processor.
- */
- RUNNABLE,
-
- /**
- * Thread state for a thread blocked waiting for a monitor lock.
- * A thread in the blocked state is waiting for a monitor lock
- * to enter a synchronized block/method or
- * reenter a synchronized block/method after calling
- * {@link Object#wait() Object.wait}.
- */
- BLOCKED,
-
- /**
- * Thread state for a waiting thread.
- * A thread is in the waiting state due to calling one of the
- * following methods:
- * <ul>
- * <li>{@link Object#wait() Object.wait} with no timeout</li>
- * <li>{@link #join() Thread.join} with no timeout</li>
- * <li>{@link LockSupport#park() LockSupport.park}</li>
- * </ul>
- *
- * <p>A thread in the waiting state is waiting for another thread to
- * perform a particular action.
- *
- * For example, a thread that has called <tt>Object.wait()</tt>
- * on an object is waiting for another thread to call
- * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
- * that object. A thread that has called <tt>Thread.join()</tt>
- * is waiting for a specified thread to terminate.
- */
- WAITING,
-
- /**
- * Thread state for a waiting thread with a specified waiting time.
- * A thread is in the timed waiting state due to calling one of
- * the following methods with a specified positive waiting time:
- * <ul>
- * <li>{@link #sleep Thread.sleep}</li>
- * <li>{@link Object#wait(long) Object.wait} with timeout</li>
- * <li>{@link #join(long) Thread.join} with timeout</li>
- * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
- * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
- * </ul>
- */
- TIMED_WAITING,
-
- /**
- * Thread state for a terminated thread.
- * The thread has completed execution.
- */
- TERMINATED;
- }
参考文章:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。