如何优化多线程上下文切换?
如果是单个线程,在 CPU 调用之后,那么它基本上是不会被调度出去的。如果可运行的线程数远大于 CPU 数 量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其它线程能够使用 CPU ,这就会导致上下文切换。
还有,在多线程中如果使用了竞争锁,当线程由于等待竞争锁而被阻塞时,JVM 通常会将 这个锁挂起,并允许它被交换出去。如果频繁地发生阻塞,CPU 密集型的程序就会发生更 多的上下文切换。
那么问题来了,我们知道在某些场景下使用多线程是非常必要的,但多线程编程给系统带来 了上下文切换,从而增加的性能开销也是实打实存在的。那么我们该如何优化多线程上下文 切换呢?这就是我今天要和你分享的话题,我将重点介绍几种常见的优化方法。
竞争锁优化
多线程对锁资源的竞争会引起上下文切换,还有锁竞争导致的线程阻塞越多,上下文切换就 越频繁,系统的性能开销也就越大。由此可见,在多线程编程中,锁其实不是性能开销的根 源,竞争锁才是。
减少锁的持有时间
我们知道,锁的持有时间越长,就意味着有越多的线程在等待该竞争资源释放。如果是 Synchronized 同步锁资源,就不仅是带来线程间的上下文切换,还有可能会增加进程间的 上下文切换。
例如,可以将一些与锁无关的代码移出同步 代码块,尤其是那些开销较大的操作以及可能被阻塞的操作。
优化前
public synchronized void mySyncMethod(){
2 businesscode1();
3 mutextMethod();
4 businesscode2();
5 }
优化后
public void mySyncMethod(){ businesscode1(); synchronized(this){mutextMethod(); }businesscode2();}
降低锁的粒度
同步锁可以保证对象的原子性,我们可以考虑将锁粒度拆分得更小一些,以此避免所有线程 对一个锁资源的竞争过于激烈。具体方式有以下两种:
锁分离
与传统锁不同的是,读写锁实现了锁分离,也就是说读写锁是由“读锁”和“写锁”两个锁 实现的,其规则是可以共享读,但只有一个写。 这样做的好处是,在多线程读的时候,读读是不互斥的,读写是互斥的,写写是互斥的。而 传统的独占锁在没有区分读写锁的时候,读写操作一般是:读读互斥、读写互斥、写写互 斥。所以在读远大于写的多线程场景中,锁分离避免了在高并发读情况下的资源竞争,从而 避免了上下文切换
锁分段
我们在使用锁来保证集合或者大对象原子性时,可以考虑将锁对象进一步分解。例如,我之 前讲过的 Java1.8 之前版本的 ConcurrentHashMap 就使用了锁分段。
wait/notify 的使用导致了较多的上下文切换
结合以下图片,我们可以看到,在消费者第一次申请到锁之前,发现没有商品消费,此时会 执行 Object.wait() 方法,这里会导致线程挂起,进入阻塞状态,这里为一次上下文切换。
当生产者获取到锁并执行 notifyAll() 之后,会唤醒处于阻塞状态的消费者线程,此时这里 又发生了一次上下文切换。 被唤醒的等待线程在继续运行时,需要再次申请相应对象的内部锁,此时等待线程可能需要 和其它新来的活跃线程争用内部锁,这也可能会导致上下文切换。 如果有多个消费者线程同时被阻塞,用 notifyAll() 方法,将会唤醒所有阻塞的线程。而某 些商品依然没有库存,过早地唤醒这些没有库存的商品的消费线程,可能会导致线程再次进 入阻塞状态,从而引起不必要的上下文切换。
推荐阅读
亿级在线百万并发认证业务分析
业务幂等性技术架构体系
记一次线上SQL死锁事故:如何避免死锁
责任链模式实践-开放银行数据保护及合规