《Java 并发编程的艺术》读书笔记 1: 说说并发编程
并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。
---《Java并发编程的艺术》
上下文切换
众所周知,及时是单核 CPU 也支持多线程执行代码,CPU 通过给每个线程分配CPU 时间片来实现这个机制。因为 CPU 时间片非常短暂,所以 CPU 通过不停地切换线程执行,来实现多线程,时间片一般都是几十毫秒。
那我们来考虑这么一种情况,在单核 CPU 环境下,我们执行多线程代码。既然是使用时间片分配算法来执行的,那假设在一次时间片分配给 A 线程的时间段内,我们的代码还没有执行完,但是CPU已经要切换到另外一个时间片上去执行 B 线程的代码了。我们怎么能保证,CPU 切换到 A 线程的时候,还能顺着这次执行到的代码位置,继续执行下去呢?
这个时候就要用到上下文切换了,CPU 通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是在切换前会保存任务的状态,以便下次切换回这个任务的时候,可以再加载这个任务的状态。这个任务(线程)从保存再到加载的过程就是一次上线文切换 。
既然我们了解到了,在单核 CPU 上运行多线程,会出现上下文切换的情况,那么是否可以说这个上下文切换是不受我们控制的对系统资源的额外占用。因为在执行上线文切换的时候既然要存储和加载,势必是有资源开销的,而且又不得不做。带着这个结论,我们再来思考下一个问题: 多线程一定快吗?或者说是不是线程数量越多,程序执行的速度越快?
答案: 不一定。我们在使用并发编程的时候,一定要考虑多个因素,一味的增加线程数量不一定会使程序运行的更快。
原因:
创建、销毁线程;上下文切换都需要一定的开销,需要消耗时间。(即使是使用线程池,那么也至少存在上线文切换的开销)
资源的限制。程序在执行过程中,部署程序的硬件服务器的许多资源是有限的,即使我们使用多线程也无法突破这个资源的瓶颈。比如:磁盘 I/O ,网络带宽,CPU 处理速度等等
死锁
只要使用过多线程,锁 是逃不开的一个话题,因为多线程的特性,我们很多情况下需要通过锁来保证代码的可见性、原子性、有序性。但是如果对其原理不了解或者使用不当,那么他也会带来一些困扰。比如下面这段代码就会引发死锁:
所谓的死锁就是多个线程互相持有锁,互相无法释放,互相等待对方释放。举个例子:
面试官说:你再和我说说什么是死锁,我就给你offer
面试者说:你给我offer我就和你说说什么是死锁
双方互相持有对方释放锁的前提条件,导致双方都无法释放锁,产生了死锁。我们上面这个例子中就是这样的:
线程 t1 持有锁 A
线程 t2 持有锁 B
线程 t1 试图去获取锁 B ,获取失败,因为锁 B 已经被线程 t2 占用,线程 t1 等待
线程 t2 试图去获取锁 A ,获取失败,因为锁 A 已经被线程 t1 占用,线程 t2 等待
至此 t1 t2 两个线程成为死锁,互不释放
如何排查是否出现了死锁?
可能在实际写代码的过程中,不会直接这么写,但是可能因为种种意外情况导致了这种死锁的产生,我们在排查问题的过程中,对于死锁是相对好发现的。首先从服务上来看,因为产生了死锁,你的服务是无法继续提供的,那么我们就可以通过dump来查看到底哪个线程出问题了。
看到这个结果,很明显是两个线程出现了死锁,这就排查问题的一个比较简单的思路,然后我们根据提示去看一下DeadLock这个类中的32和49行的代码即可。
如何避免死锁
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源
尝试使用定时锁(lock.tryLock(timeout))来代替内部锁
对于数据库锁,加锁和解锁必须在同一个数据库连接里
评论