并发中的一些基础知识和问题
1.多线程原理
在操作系统课中线程讲的不多,讲的比较多的是进程。在操作系统中,进程是资源分配的基本单位,操作系统课主要讲的就是系统资源分配的问题,操作系统系统的主要任务也就是软硬件的资源管理。计算机资源包括硬盘、网络、CPU、内存等等。而线程是CPU多机调度的基本单位,多机调度系统中,为了充分利用CPU资源,操作系统会把CPU的时间分成非常多非常短的时间片,每一个时间片就是一个线程上CPU运行的时间段,CPU为一个线程的任务工作一个时间片后,将会进行
上下文切换,所谓上下文切换就是要保存当前线程执行的状态,以便下次切回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。由于时间片非常短(一般几十毫秒),而且切换间隔也非常短(可以忽略不计,当然在特定分析情景下也不能忽略不计),所以用户感觉就像多个线程在同时工作一样,比如说我们可以开一个计算程序的同时打开一个word编辑,这时候CPU就会分给计算程序线程一些时间片,也分给编辑线程一些时间片,通过多机调度算法让计算线程和编辑线程交替得到CPU资源,由此来实现让用户感觉同时在计算和编辑的体验,而且这更加充分地利用了CPU资源(其实性能优化从一方面讲就是榨干计算机的各种资源,这样讲是片面的,好的优化应该是让资源尽量分给有效的工作,减少无效的工作)。
2.多线程的挑战
前面说到了上下文切换,虽然上下文切换所需要的时间特别短,但我们有时却不得不关注这一点,因为切换如果非常频率,也要造成资源的浪费,并且在有一些情况下,资源会大量被上下文切换占据,用了多线程甚至比不用效率更低。因此,多线程的挑战就是如何设计好上下文的切换,尽量避免不必要的上下文切换,发挥多线程的优势,让CPU的工作效率更高。
3.减少上下文切换的方案
(1)无锁并发编程
在多线程编程时,我们经常会使用锁来解决多线程竞争共享资源的问题。锁在一定程序会让多线程程序正确的执行,好的锁运用设计确实能解决很多问题,但使用锁最大的问题就是多线程竞争锁时会引起上下文切换,而且锁把会临界区(就是锁住的区域)执行变成串行的。在阿里的Java编程规范中就明确说明能用段锁就不要方法锁,能用对象锁就不要用类锁,能不用锁就不要用。在处理数据时,有时候,我们可以通过一些方法来避免使用锁,比如我们可以通过Hash算法来将计算任务分成互不相交的若干子任务,由于不共享数据,就不需要加锁。
(2)CAS算法
JDK从1.5开始提供了java.util.concurrent.atomic包,通过使用cas算法可以在多线程环境下进行原子操作,由于是原子操作,我们就不再需要加锁了。这样就在保证程序的正确性的同时避免使用锁来提高程序的性能。
(3)使用最少线程
线程是一种重要的资源,创建线程需要分配线程栈程序计数器等资源。有时候我们可能不需要开启那么多线程,因此在编程时如果可以少开一个线程就尽量少用线程。在java中默认提供了线程池,我们也可以通过线程池来复用线程,减少线程带来的资源消耗。
4.死锁
死锁是一种资源申请循环等待的情况,学过操作系统的小伙伴们肯定不会陌生(这是操作系统的必考题,每年如此),死锁一般都是锁运用设计不当造成的。比如有A、B两个资源,甲乙两个线程,有一个任务需要同时拥有AB两个资源,但如果在某个时刻,甲拥有了A资源,乙拥有了B资源,所以甲将等待直到能够获得B资源,乙也会等待直接能够获得A资源,并且两个线程都会占有已有的资源不释放,甲乙两个线程将会一直等待下去,这样就会产生死锁,当然举的这个例子很明显能看出死锁,但在实际的软件开发中,情况将会复杂得多。
死锁产生的四必要条件是
(1)互斥:一个资源只能被一个线程得到
(2)请求保持:就是一个线程得到资源后要等到任务完成时才会释放
(3)非抢占式:在资源未使用完之前,一个线程不能抢夺其他线程的资源
(4)循环等待:多个线程等待资源形成一个等待环。
这四个条件要同时满足才能产生死锁,因此要避免或解除死锁状态只需要破坏这四个条件的任一就可以了。在操作系统学习时,我们学过的银行家算法、生产者-消费者队列等是不错的解决思想,现实中也有很多由这些思想创造出来的产品和解决方案。
当然,我们可以通过遵循一些原则来尽量避免死锁:
避免一个线程同时获取多个锁:这样就不会产生循环等待环
避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占有一个资源
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制:这样就算形成了死锁也可以在过期时脱离死锁状态
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现锁失败的情况
在实现开发过程中,这些原则有可能不能遵守,但我们可以根据实际情况分析,这些原则只能直到指导作用。
5.资源受限
有时候,由于硬件的限制,多线程并不能帮我们提高性能,比如说带宽只有2Mb/s,某个资源的下载速度是1Mb/s,就算我们开10个线程,下载速度也永远达不到10Mb/s,并且由于上下文切换,反而会导致其他一些资源浪费,从而造成性能下降,因为资源受限会将并发变成串行。
资源受限的解决方案通常是建立多物理分布式环境,比如说Hadoop搭建服务器集群,通过多物理资源来分担任务,虽然达不到资源的1+1=2的效果,但也能在一定情况一定程度上解决资源受限的问题。
在实际的多线程编程中,我们可以借鉴资源复用或者计算机体系结构中的缓冲思想来构建一些解决方案,比如网络数据的读写。或者还可以通过异步编程的方式来避免并发转串行的尴尬。