JAVA并发编程(九)之多线程未必快
多线程在某些情况下可以提升程序的性能,但是不合理的使用多线程并不一定能提升程序的执行效率
一、多线程并不一定比串行快
这段代码窜行的执行速度,不比并发的慢
二、线程并发执行的原理
单核cpu的并发执行:由于单核CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢? 操作系统的设计者 巧妙地利用了时间片轮转的方式,时间片轮询将引发一个新的技术问题,就是线程的上下文切换。
线程上下文切换:CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行,这段过程就叫做上下文切换。
- 切出: 一个线程被剥夺处理器的使用权而被暂停运行,将当前线程的运行信息保存起来
- 切入: 一个线程被系统选中占用处理器开始或继续运行,CPU需要将改线程切出时的数据加载到寄存器与程序计数器
线程信息:每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。
寄存器: 是 CPU 内部的数量较少但是速度很快的存储区域,主要用来提高计算机程序运行的速度。
程序计数器PC:是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。
上下文切换的消耗:
- 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
- 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小
上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。
线程让出cpu的情况:
- 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上、获取锁失败
- 当前运行线程结束,即运行完run()方法里面的任务
引起线程上下文切换的因素
- 当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
- 中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
- 用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
- 多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换
三、如何减少上下文切换
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。