多线程相关概念整理

来源:互联网 发布:李国杰 大数据 编辑:程序博客网 时间:2024/06/06 04:00
一、为什么要使用多线程(并发与并行)
 如果仅针对单核CPU,多线程适合于任务执行期间需要等待返回结果的场景。一个任务进入等待后,CPU进入空闲,这时把CPU资源分配给其他线程执行。这个多个任务在单CPU上的切换,就是并发。
CPU在线程上切换的时间一般小于任务等待的时间,所以多个线程并发执行会提高程序执行效率。而对于多核CPU或者多个CPU的,CPU除了并发外,多个CPU上不同线程还能同时的处理不同的任务,这就是并行。

二、多个单核CPU与多核CPU
 对于多个CPU,每个CPU都有自己独立的电路支持,有自己的Cache,它们之间通过板上的总线进行通信。执行多线程程序时,每个线程跑在一个独立的CPU上,线程间的所有协作都要走总线,而共享的数据更有可能要在好几个Cache同时存在。
对于多核CPU,我们只需要一套芯片组、一套存储,多核之间通过芯片内部总线进行通信,共享内存。所以多核CPU在跑同一个多线程程序时效率要高。

三、锁的发展
在JDK5之前,Java的多线程(包括它的性能)一直是个软肋,只有synchronized、Thread.sleep()、Object.wait/notify这样有限的方法,而synchronized的效率还特别地低,开销比较大。
在多线程上有了彻底的提高,其引进了并发编程大师Doug Lea的java.util.concurrent包(后面简称J.U.C),支持了现代CPU的CAS原语,不仅在性能上有了很大提升,在自由度上也有了更多的选择,此时J.U.C的效率在高并发环境下的效率远优于synchronized。
但JDK6中对synchronized的内在机制做了大量显著的优化,加入了CAS的概念以及偏向锁、轻量级锁,使得synchronized的效率与J.U.C不相上下,并且官方说后面该关键字还有继续优化的空间,所以在现在JDK7的时代,synchronized已经成为一般情况下的首选,在某些特殊场景——如可中断的锁、条件锁、等待获得锁一段时间如果失败则停止——下,J.U.C是适用的。

四、锁的特性
锁机制的两种特性:
互斥性:即同一时间只允许一个线程持有某个对象的锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的,否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

五、挂起、休眠、阻塞和非阻塞
挂起:当线程被挂起时,其会失去CPU的使用时间,直到被其他线程(用户线程或调试线程)唤醒。
休眠:同样是会失去CPU的使用时间,但是在过了指定的休眠时间之后,它会自动激活,无需唤醒(整个唤醒表面看是自动的,但实际上也得有守护线程去唤醒,只是不需编程者手动干预)。
阻塞:在线程执行时,所需要的资源不能得到,则线程被挂起,直到满足可操作的条件。
非阻塞:在线程执行时,所需要的资源不能得到,则线程不是被挂起等待,而是继续执行其余事情,等待条件满足了后,收到了通知(同样是守护线程去做)再执行。
挂起和休眠是独立的操作系统概念,而阻塞和非阻塞则是在资源不能得到时的两种处理方式。
在Java中显式的挂起原先是通过Thread的suspend方法来体现,现在此概念已经消失,原因是suspend/resume方法已经被废弃,它们容易产生死锁。
当suspend的线程持有某个对象锁,而resume它的线程又正好需要使用此锁的时候,死锁就产生了。
所以在现在的JDK版本中,挂起是JVM的系统行为,程序员无需干涉。休眠的过程中也不会释放锁,但它一定会在某个时间后被唤醒,所以不会死锁。现在我们所说的挂起,往往并非指编写者的程序里主动挂起,而是由操作系统的线程调度器去控制。
wait/notify,这两个方法同样是等待/通知,但它们的前提是已经获得了锁,且在wait(等待)期间会释放锁,并等待唤醒。当被其他线程唤醒后,并没有马上重新获取锁,而只是获得了重新获取锁的机会。

六、自旋锁和阻塞锁
 自旋锁:自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停的增加时,性能下降明显,因为每个线程都要执行,占用CPU时间。如果线程竞争不激烈,适合使用自旋锁。
 阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。
 
七、偏向锁、轻量锁和重量锁
 参考之前文章:http://blog.csdn.net/lanxiangru/article/details/78355718

八、线程池
为什么要使用线程池?
 单个线程完成任务的时间主要分为:线程的创建时间、线程内任务的执行时间、线程的销毁时间。如果线程的创建和销毁时间占用整个流程的比重比较大,可以通过线程池来改善。线程池会把线程的创建、销毁尽量安排在程序的启动和结束以及一些空闲时间。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目。
线程池组成:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
原创粉丝点击