分析&回答
注意误区
- 并发编程并不是银弹,做为一种基于共享内存的并发编程,它会存在很多编程上的问题,比如:上下文切换、死锁、线程饥饿、竞争条件、Bug难以重现和定位
- 并发编程的目的是 为了让程序运行得更快。但是, 并不是启动更多的线程就能让程序最大限度地并发执行。
- 多线程不一定快,这是因为线程有创建和上下文切换的开销。
上下文切换问题
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。
时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
-
无锁并发编程
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。 -
CAS算法
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。 -
使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。 -
协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
死锁问题
参考:Java如何检测死锁?
饥饿
饥饿,线程因无法访问所需资源而无法执行下去的情况。
“不患寡,而患不均”,如果线程优先级“不均”,在CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
解决“饥饿”问题的方案很简单,有三种方案:
- 一是保证资源充足,
- 二是公平地分配资源,
- 三就是避免持有锁的线程长时间执行。
这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。倒是方案二的适用场景相对来说更多一些。
竞争条件
竞态条件是指同一个程序多线程访问同一个资源,如果对资源的访问顺序敏感,就称存在竞态条件,代码区成为临界区。最常见的竞态条件为:先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
那么我们如何解决这个问题呢?
- 第一种办法:使用synchronized
- 第二种办法:使用CAS
反思&扩展
如何测试上下文切换次数和时长
- 使用Lmbench3可以测量上下文切换的时长
- 使用vmstat可以测量上下文切换的次数。
喵呜面试助手: 一站式解决面试问题,你可以搜索微信小程序 [喵呜面试助手] 或关注 [喵呜刷题] -> 面试助手 免费刷题。如有好的面试知识或技巧期待您的共享!