乐趣区

关于java:线程上下文切换

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 线程上下文切换

退出移动版