本文参考于《Java并发编程的艺术》
1、 总述
并发编程的目的是为了让程序运行得更快
,但是,并不是启动更多的线程就能让程序最大限度地并发执行
。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题
、死锁的问题
,以及受限于硬件和软件的资源限制问题
。
2、线程上下文切换
2.1、切换过程
CPU通过时间片分配算法来循环执行任务
,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态
,以便下次切换回这个任务时,可以再加载这个任务的状态
。所以任务从保存到再加载的过程就是一次上下文切换。
2.2、 线程上下文切换的缺点
上下文切换会消耗大量时间
,从而影响多线程的执行速度。
2.3、多线程一定快吗?
不一定。
原因:因为线程有创建和上下文切换的开销
。
2.4、如何减少上下文切换?
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以
用一些办法来避免使用锁
,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。 - CAS算法:Java的Atomic包
使用CAS算法来更新数据
,而不需要加锁。 - 使用最少线程:
避免创建不需要的线程
,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。 - 使用协程:
在单线程里实现多任务的调度
,并在单线程里维持多个任务间的切换
。协程不是进程也不是线程,而是一种特殊的函数
,这个函数可以在某个地方挂起,也可以从挂起处继续执行。一个线程的多个协程是串行执行的,当一个协程执行时,其他协程必须挂起。使用协程不再需要陷入系统的内核态
,执行效率非常高
,没有线程切换的开销
,更不需要多线程的锁机制
。
3、死锁
3.1、 什么是死锁?
线程死锁描述的是这样一种情况:多个线程同时被阻塞
,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞
,因此程序不可能正常终止。
3.2、 产生死锁的条件
1. 互斥条件:该资源任意一个时刻只由一个线程占用
。
2. 请求与保持条件:一个进程因请求资源而阻塞时
,对已获得的资源保持不放
。
3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺
,只有自己使用完毕后才释放资源。
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
。
3.3、 如何预防死锁?
1. 破坏请求与保持条件 :一次性申请所有的资源
。
2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
。
3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放
。破坏循环等待条件。
3.4、 如何避免死锁?
避免死锁就是在资源分配时,借助于
算法(比如银行家算法)
对资源分配进行计算评估,使其进入安全状态。
4、资源限制问题
4.1、什么是资源限制?
资源限制是指在进行并发编程时,
程序的执行速度受限于计算机硬件资源或软件资源
。
4.2、资源限制引发的问题
将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行
,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行
,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
4.3、如何解决资源限制的问题?
- 对于硬件资源限制,可以考虑
使用集群并行执行程序
。既然单机的资源有限制,那么就让程序在多机上运行。 - 对于软件资源限制,可以考虑
使用资源池将资源复用
。
4.4、在资源限制情况下怎样进行并发编程?
根据不同的资源限制调整程序的并发度
,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。