并发编程基础 - 多线程的上下文切换问题

目录

1、什么是线程上下文

2、哪些可以引发上下文切换

3、怎么查看上下文切换

4、怎么减少上下文切换,对实际的应用场景的理解


    记得在两年前,翻开《Java并发编程的艺术》开篇就讲的上下文切换可能使多线程比串行执行还慢,那时还是在硬看书的阶段上来直接就干懵了。并且在很多场景下串行的效率就是比并行还快,比如Redis、Nginx,但是他们的前提是场景问题,主要的问题是在于处理IO问题,于是引入Reactor模型(select、poll、epoll)的支持,所以个人理解还是要对自己项目的性质有比较深的理解,合适的才是最好的(比如线程池数量的设置)。所以多线程确实能解决问题但是也能带来问题,算是双刃剑,所以搞清上下文切换的原因,怎么监控,已经解决就比较有实际意义了。我使用线程池很久之后,一直觉得按照IO型、CPU型先设置线程数,最后根据压测找到最高点(根据理论,到最高点后会往下走),现在才意思到到上下文切换的指标获取也是一个佐证,更好的理解和设置线程数【当然这只是其中一种场景,也是我很多时候纠结和思考的点】。

1、什么是线程上下文

    在并发理论基础 - 并发问题的背景和根源(原子性、可见性、有序性)中就理解到,为了解决个硬件发展速度不一致的问题,引入了分时CPU的时间片(在单核情况下,操作系统也能处理并发任务,表现为我们能一遍听歌一遍敲代码),处理器给每个线程分配CPU时间片(Time Slice),一般为即使毫秒,并且有操作系统控制切换,由于时间太短我们根本感知不到,所以看上去像是同时发生的一样。时间片用完或者被迫终止等情况就会发现另一个线程来执行CPU时间片,成为上下文切换(Context Switch)。

    一次上下文切换,需要保存和恢复切入切除的线程的进度信息,包括了程序计数器存储内容和指令等。这与并发编程基础 - Thread状态和生命周期中提到的线程状态装换有密切的关系,也与并发编程基础 - synchronized锁优化提到的管程模型中,线程进入不同的条件列表阻塞等有关,当线程阻塞、挂起时就会发生用户态和内核态的切换,就会发生上下文切换。并且代价是比较昂贵的,如果操作系统将单核CPU轮流分配给线程执行任务还好,但是现在的计算机都是多CPU(多核心线程),那么发生跨CPU的山下文切换就更加昂贵了。上下文切换引发的开销包括:

操作系统保存和恢复上下文;

调度器进行线程调度;

处理器高速缓存重新加载;

上下文切换也可能导致整个高速缓存区被冲刷,从而带来时间开销

2、哪些可以引发上下文切换

    发生上下文切换有两种情况:1、程序自己触发的,自发性上下文切换(比如我们使用了synchronized关键字);2、由操作系统调度或者虚拟机引发的自发性上下文切换。

    1)、自发性上下文切换

  1. sleep
  2. wait
  3. yield
  4. join
  5. park
  6. synchronized
  7. lock

    2)、非自发性上下文切换

  1. 分配的时间片用完(操作系统调度)
  2. gc的STW(Stop the World)阶段

3、怎么查看上下文切换

package com.kevin.multithreading.geektime;

/**
 *  证明多线程未必快,已经线程上下文切换的影响非常大
 * @author kevin
 * @date 2020/10/29 0:09
 * @since 1.0.0
 */
public class ContextSwitchDemo {
    public static void main(String[] args) {
        //运行多线程
        MultiThreadTester test1 = new MultiThreadTester();
        test1.Start();
        //运行单线程
        SerialTester test2 = new SerialTester();
        test2.Start();
    }

    public static class MultiThreadTester extends ThreadContextSwitchTester {
        @Override
        public void Start() {
            long start = System.currentTimeMillis();
            MyRunnable myRunnable1 = new MyRunnable();
            Thread[] threads = new Thread[12];
            //创建多个线程
            for (int i = 0; i < 12; i++) {
                threads[i] = new Thread(myRunnable1);
                threads[i].start();
            }
            for (int i = 0; i < 12; i++) {
                try {
                    //等待一起运行完
                    threads[i].join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            long end = System.currentTimeMillis();
            System.out.println("multi thread exce time: " + (end - start) + "s");
            System.out.println("counter: " + counter);
        }
    // 创建一个实现Runnable的类
    class MyRunnable implements Runnable {
        public void run() {
           while (counter < 100000000) {
                  synchronized (this) {
                         if(counter < 100000000) {
                                increaseCounter();
                         }

                  }
           }
        }
    }
}

  //创建一个单线程
   static class SerialTester extends ThreadContextSwitchTester{
          @Override
          public void Start() {
                 long start = System.currentTimeMillis();
                 for (long i = 0; i < count; i++) {
                       increaseCounter();
                 }
                 long end = System.currentTimeMillis();
                 System.out.println("serial exec time: " + (end - start) + "s");
                 System.out.println("counter: " + counter);
          }
   }

   //父类
   static abstract class ThreadContextSwitchTester {
          public static final int count = 100000000;
          public volatile int counter = 0;
          public int getCount() {
                 return this.counter;
          }
          public void increaseCounter() {

                 this.counter += 1;
          }
          public abstract void Start();
   }
}

    叠加demo,使用synchronized关键字(会引发上下文切换),我的电脑是6核心12线程,demo中使用了12线程。与单线程的程序进行对比,线上环境非常复杂我们很可能写这样的代码。下面是我的执行结果:

    上面的demo证明了使用多线程(线程池)未必就快,特别是在CPU型任务中,也证明了上下文切换过于频发可能还没有串行的效率高。当然,如果在之前我肯定就懵逼了,就搞不懂多线程有上面意思。但是在高并发的项目中,线程池并且并行任务任然是提高Tps、以及单个复杂接口的重要手段之一。那么,监控、压测获取最佳线程数、发生问题时查找,都需要有量化的线程上下文切换指标。

1)、Linux

    Linux中可以使用vmstat 命令来查看系统上下文切换的频率,cs参数(意思就是 Context Switch),一般压测最高在几万还是能接受的,但是如果几十上百万就肯定有问题了,需要项目经验的积累。

使用pidstat 命令查看指定进程的上下文切换,比如我们的某一程序:

2)、Windows

    Windows下可以使用Process Explorer来查看程序执行时的上下文切换次数,下载地址:百度网盘 请输入提取码,提取码:abcd。选择View - > Select Columns;主要是在Process Image以及Process Performance【在其中选择 Threads和Context Switches】,如下:

4、怎么减少上下文切换,对实际的应用场景的理解

    知道了是什么引发了上下文切换,那么怎么进行减少,就非常有必要了,直接关乎使用场景,或者问题的查找,非常重要:

1)、JVM做出的努力

    在并发编程基础 - synchronized锁优化中,我们理解了JVM针对不同并发级别的优化(偏向锁没有用户内核态切换则没有上下文切换等等),以及编译时锁消除、锁粗化

2)、synchronized

    使用细粒度锁,本质上也是在减少锁持有的时间。

3)、juc

   juc中使用volatile + Cas机制实现,而volatile保证了可见性和有序性,并不会引起上下文的切换,atomic包下面的类就不会引起线程上下文切换。除非是使用AQS管程模型的队列操作,Lock 与Condition配合,则会使用LockSupport的park等操作。

4)、合理设置线程池大小

    特别是在处理CPU型任务时,并行度就是核心线程数,过多的线程只能增加线程上下文的切换。这个在后面线程池中详细分析,因为这与任务的性质,线程池在原理,使用的队列等都息息相关。

5)、减少gc中的STW

    当gc线程时,工作线程会记录状态等,特别是在Stop The World时(所以工作线程都停止),对业务本身影响最大。那么合理的JVM调优就非常重要了。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值