Java多线程调优之减少上下文切换

在这里插入图片描述

并发编程的目的是为了让程序运行得更快,但并不是启动更多的线程就能让程序最大限度地并发执行,在进行并发编程时,会面临非常多的挑战,比如上下文切换的问题、死锁的问题,本文将介绍多线程中上下文切换带来的性能问题。

上下文切换简介

处理器给每个线程分配 CPU 时间片,线程在分配获得的时间片内执行任务,CPU 时间片是 CPU 分配给每个线程执行的时间段,一般为几十毫秒。

当一个线程的时间片用完了,或者因自身原因被迫暂停运行了,另外一个线程(可以是同一个线程或者其它进程的线程)就会被操作系统选中,来占用处理器。这种一个线程被暂停剥夺使用权,另外一个线程被选中开始或者继续运行的过程就叫做上下文切换(Context Switch)

在上下文切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

多线程并行一定比串行快吗?

我们常常听说要提升系统性能要使用多线程,那么多线程是不是万能钥匙呢? 下面我们给出一段代码,来进行累加求和操作,对比串联执行和并发执行的速度。

代码


public class DemoApplication {
       public static void main(String[] args) {
              //运行多线程
              MultiThreadTester test1 = new MultiThreadTester();
              test1.Start();
              //运行单线程
              SerialTester test2 = new SerialTester();
              test2.Start();
       }
       
       
       static class MultiThreadTester extends ThreadContextSwitchTester {
              @Override
              public void Start() {
                     long start = System.currentTimeMillis();
                     MyRunnable myRunnable1 = new MyRunnable();
                     Thread[] threads = new Thread[4];
                     //创建多个线程
                     for (int i = 0; i < 4; i++) {
                           threads[i] = new Thread(myRunnable1);
                           threads[i].start();
                     }
                     for (int i = 0; i < 4; i++) {
                           try {
                                  //等待一起运行完
                                  threads[i].join();
                           } catch (InterruptedException e) {
                                  // TODO Auto-generated catch block
                                  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();
       }
}

在这个示例中,答案是“不一定”,测试结果如下图所示:

通过数据对比我们可以看到:串联的执行速度比并发的执行速度要快。这就是因为线程的上下文切换导致了额外的开销,使用 Synchronized 锁关键字,导致了资源竞争,从而引起了上下文切换。

Redis 、 NodeJS的设计就很好地体现了单线程串行的优势。

在 Linux 系统下,可以使用 Linux 内核提供的 **vmstat **命令,来监视 Java 程序运行过程中系统的上下文切换频率,cs 如下图所示:

线程越多,系统的运行速度不一定越快。那么我们平时在并发量比较大的情况下,什么时候用单线程,什么时候用多线程呢?

一般在单个逻辑比较简单,而且速度相对来非常快的情况下,我们可以使用单线程。例如,我们前面讲到的 Redis,从内存中快速读取值,不用考虑 I/O 瓶颈带来的阻塞问题。

在逻辑相对来说很复杂的场景,等待时间相对较长又或者是需要大量计算的场景,建议使用多线程来提高系统的整体性能。例如,NIO 时期的文件读写操作、图像处理以及大数据分析等。

如何优化多线程上下文切换?

多线程对锁资源的竞争会引起上下文切换,还有锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销也就越大。由此可见,在多线程编程中,归根结底就是减少锁的竞争,可以采取以下几种方式:

减少锁的持有时间

锁的持有时间越长,就意味着有越多的线程在等待该竞争资源释放。如果是 Synchronized 同步锁资源,就不仅是带来线程间的上下文切换,还有可能会增加进程间的上下文切换。

可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作。

降低锁的粒度

同步锁可以保证对象的原子性,我们可以考虑将锁粒度拆分得更小一些,以此避免所有线程对一个锁资源的竞争过于激烈。

具体方式有以下两种:

  1. 一是锁分离,与传统锁不同的是,读写锁实现了锁分离,也就是说读写锁是由“读锁”和“写锁”两个锁实现的,在多线程读的时候,读读是不互斥的,读写是互斥的,写写是互斥的。而传统的独占锁在没有区分读写锁的时候,不管读写都互斥。所以在读远大于写的多线程场景中,锁分离避免了在高并发读情况下的资源竞争,从而避免了上下文切换,就比如ReentrantReadWriteLock

  2. 二是锁分段我们在使用锁来保证集合或者大对象原子性时,可以考虑将锁对象进一步分解。例如: Java1.8 之前版本的 ConcurrentHashMap 就使用了锁分段。

非阻塞乐观锁CAS替代竞争锁

Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

协程

协程是一种比线程更加轻量级的东西,相比于由操作系统内核来管理的进程和线程,协程则完全由程序本身所控制,也就是在用户态执行。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

PS: Java语言层面不支持,需要引入第三方框架实现

wait/notify 优化

在 Java 中,我们可以通过配合调用 Object 对象的 wait() 方法和 notify() 方法或 notifyAll() 方法来实现线程间的通信。

建议使用Lock 锁结合 Condition 接口替代 Synchronized 内部锁中的 wait / notify,实现等待/通知。这样做不仅可以解决上述的 Object.wait(long) 无法区分的问题,还可以解决线程被过早唤醒的问题。

合理地设置线程池大小,避免创建过多线程

线程池的线程数量设置不宜过大,因为一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换。

总结

上下文切换是多线程编程性能消耗的原因之一,而竞争锁、线程间的通信以及过多地创建线程等多线程编程操作,都会给系统带来上下文切换

除此之外,I/O 阻塞以及 JVM 的垃圾回收也会增加上下文切换。总的来说,过于频繁的上下文切换会影响系统的性能,所以我们应该避免它。

好了,以上就是今天分享的全部内容,enjoy ~ 欢迎吐槽、交流 微信公众号【AI黑板报】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值