1.1 概念
单核处理器也反对多线程执行代码,CPU 通过给每个线程调配 CPU 工夫片 来实现这个机制。(工夫片是 CPU 调配给各个线程的工夫,因为工夫片十分短,个别是几十毫秒,所以 CPU 通过不停地切换线程执行,让咱们感觉多个线程是同时执行的)。
CPU 通过工夫片调配算法来循环执行工作,当前任务执行一个工夫片后会切换到下一个工作。然而,在切换前会保留上一个工作的状态,以便下次切换回这个工作时,能够再加载这个工作的状态。所以 工作从保留到再加载的过程就是一次上下文切换(Context Switch)。
<img src=”https://img-blog.csdnimg.cn/20201022161036944.png” style=”zoom:80%;” />
可见,线程上下文切换的过程,就是一个线程被 暂停剥夺 使用权,另一个线程被 选中开始 或者 持续运行 的过程。
1.2 案例阐明
public class ContextSwitchTest {
private static final long count = 10000;
public static void main(String[] args) throws Exception {serial();
concurrency();}
// 串行
private static void serial() {long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {a += 5;}
int b = 0;
for (int i = 0; i < count; i++) {b--;}
long time = System.currentTimeMillis() - start;
System.out.println("Serial:" + time + "ms, b =" + b + ", a =" + a);
}
// 并发
private static void concurrency() throws Exception {long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {public void run() {
int a = 0;
for (int i = 0; i < count; i++) {a += 5;}
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {b--;}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("Concurrency:" + time + "ms, b =" + b);
}
}
测试后果:
循环次数 | 串行执行耗时 /ms | 并发执行耗时 /ms |
---|---|---|
1 亿 | 139 | 108 |
1000 万 | 16 | 14 |
100 万 | 6 | 6 |
10 万 | 2 | 4 |
1 万 | 0 | 3 |
察看可知,当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。正是因为线程有创立和上下文切换的开销,所以才会呈现这种景象。
1.3 切换查看
在 Linux 零碎下能够应用 vmstat 命令来查看上下文切换的次数,上面是利用 vmstat 查看上下文切换次数的示例:
[root@localhost vagrant]# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 628740 2068 218576 0 0 1085 40 136 324 2 3 95 0 0
0 0 0 628748 2068 218576 0 0 0 0 48 86 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 35 76 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 41 82 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 35 82 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 39 78 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 38 88 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 45 80 0 1 99 0 0
0 0 0 628748 2068 218576 0 0 0 0 34 77 0 0 100 0 0
0 0 0 628748 2068 218576 0 0 0 0 40 87 0 0 100 0 0
vmstat 1 指每秒计数一次,cs 示意上下文切换的次数。能够看到,上下文每秒钟切换 80~90 次左右。
1.4 切换起因
对于咱们常常应用的 抢占式操作系统 而言,引起线程上下文切换的起因大略有以下几种:
- 以后执行工作的工夫片用完之后,零碎 CPU 失常调度下一个工作。
- 以后执行工作碰到 IO 阻塞,调度器将此工作挂起,持续下一工作。
- 多个工作抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,持续下一工作。
- 用户代码挂起当前任务,让出 CPU 工夫。
- 硬件中断。
Java 程序 中,线程上下文切换的次要起因可分为:
-
程序自身触发的 自发性上下文切换
- sleep、wait、yield、join、park、synchronized、lock 等办法
-
零碎或虚拟机触发的 非自发性上下文切换
- 线程被调配的 工夫片用完 、JVM 垃圾回收(STW、线程暂停)、 执行优先级高的线程
在 Java 虚拟机中,由程序计数器(Program Counter Register)存储 CPU 正在执行的指令地位、行将执行的下一条指令的地位。
1.5 缩小上下文切换
缩小上下文切换的办法 有无锁并发编程、CAS 算法、应用起码线程和应用协程。
- 无锁并发编程。多线程竞争时,会引起上下文切换,所以多线程解决数据时,能够用一些方法来防止应用锁,如将数据的 ID 依照 Hash 取模分段,不同的线程解决不同段的数据。
- CAS 算法。Java 的 Atomic 包应用 CAS 算法来更新数据,而不须要加锁。
- 应用起码线程。防止创立不须要的线程,比方工作很少,然而创立了很多线程来解决,这样会造成大量线程都处于期待状态。
- 协程。在单线程里实现多任务的调度,并在单线程里维持多个工作间的切换。
参考资料
JAVA 并发编程的艺术
定位常见 Java 性能问题
Java 线程上下文切换