并发与并行
- 并发:指在同一时间段内,有多个任务在交替执行。
- 并行:指在同一时间段内,有多个任务同时执行。
上下文切换
什么是上下文切换?
多线程编程中线程的个数大于 CPU 核心的个数,为了让这些线程都能得到有效执行或者为了使单核处理器支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,Windows的时间片一般是几毫秒到几十毫秒(ms),而Linux的相对固定。上下文切换通常是计算密集型的,上下文切换对系统来说意味着消耗大量的 CPU 时间。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
多线程就一定快吗?
多线程不一定会快,线程的创建和上下文的切换会导致并发执行的速度比串行慢,当CPU产生浪费比例比较大的时候使用多线程才比较合适
测试上下文切换的工具
- Lmbench测量上下文切换的时长
- vmstat测量上下文切换切换的次数
减少上下文切换的办法
- 无锁并发编程。可以使用一些方法来避免锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法。Java的Atomic包使用CAS来更新数据,不需要加锁。
- 使用最少线程。避免创建不需要的线程。
- 使用协程。在单线程实现多任务的调度,并在单线程里维持多个任务间的切换。
死锁
什么是死锁?如何避免死锁?
- 死锁
- 锁作为元数据存储在Java对象的头部结构中。对象的头部结构包含了关于对象本身的信息,例如哈希码、GC年龄、锁状态信息等。当我们说“在对象上加锁”时,实际是指将这个对象的锁状态信息修改为锁定状态。
- 当一个线程竞争资源失败,他会进入阻塞队列并让出CPU,并且不会释放它当前已经持有的任何锁。
- 如图:线程t1和线程t2他们同时都想申请对方的资源,就会互相等待对方释放锁,引起死锁
- 避免死锁的办法
《Java并发编程的艺术》和《操作系统》中有不同的说法。- 《Java并发编程的艺术》
- 避免同一个线程同时获得多个锁
- 变量一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用
lock.tryLock(timeout)
来代替使用内部锁机制 - 对于数据库锁,加锁解锁必须在一个数据库连接里,否则会出现解锁失败的情况
- 《操作系统》
- 破坏“请求与保持”条件 :所有进程一次性申请所有的资源。
- 破坏“不剥夺”条件 :对于一个已经保持某些资源的进程,如果申请不到其他新的资源,就必须释放所有保持的资源。
- 破坏“环路等待”条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
- 《Java并发编程的艺术》