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亿139108
1000万1614
100万66
10万24
1万03

察看可知,当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。正是因为线程有创立和上下文切换的开销,所以才会呈现这种景象。

1.3 切换查看

在Linux零碎下能够应用vmstat命令来查看上下文切换的次数,上面是利用vmstat查看上下文切换次数的示例:

[root@localhost vagrant]# vmstat 1procs -----------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 线程上下文切换