Java多线程第一篇--聊聊并发

212 阅读7分钟

并发编程的产生

在计算机发展的进程中,由最初的单核CPU发展到至今的多核CPU,甚至在某些的专业的服务器上可能会达到多个CPU一起运行的情况。因此在多核CPU的硬件基础上,我们对编程的要求也随之提升了,催生出了并发编程/多线程开发,通过这些手段,我们可以将硬件的计算能力发挥到极致,从而使得我们开发出的系统在性能上得到了极大的提升。

此外,在目前大型的企业应用、互联网应用、物联网应用中,比如电商下订单这一个场景,在传统单体架构中,往往生成订单,减库存,扣余额是在一个线程事务同步解决的,但是随着平台用户流量等的增加,传统架构明显不能满足,为了提升整体系统的性能,我们往往需要对当前业务进行拆分,这时可以简单的利用多线程编程(即异步/或者开启通知等形式)先将主要业务在当前线程完成,通过异步通知/调用的方式将次要业务完成,以此达到系统业务的最终一致。

再举例一个简单的场景,比如现在业务需要统计一个大型报表,需要对一个甚至多个表进行查询操作,通常的做法就是,直接开启循环查询数据库,往往得到的效果就是前端卡住甚至直接卡死超时,甚至在一些极端的情况下导致数据库表锁死,从而影响正常的业务流程使用,此时我们仅需要将一个线程里做的事情,分配到多个线程中进行,最终通过Java的并发组件倒计时器唤醒主线程进行最后的数据处理。

通常在我们面对复杂的业务模型时,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。

并发编程的缺点,存在的问题

优点

以上介绍了并发编程的由来,其实也就介绍了并发编程的优点,总结如下:

  • 充分的利用了计算机多核CPU的计算能力,从而提升系统的性能
  • 面对复杂的业务模型时,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分

缺点

1、上下文切换频繁

即使是单核CPU,也是可以进行多线程的开发的,CPU给每个线程分配CPU时间片(CPU时间片是CPU分配给各个线程的时间)来实现这个机制。但是时间片非常短,所以CPU要不断的切换线程,让我们以为是多线程执行。而每次切换线程,都需要保存当前的状态,一边能够进行恢复先前状态,(每次从保存状态到再加载的过程就是一次上下文的切换)而这个切换是非常消耗性能的,过于频繁的切换线程反而会降低并发编程的优势。

如何减少上下文切换次数

  • 无锁并发编程:如在多线程处理数据时,可以用一些方法可以避免使用锁,利用IDhash算法取模分段,不同的线程处理不同段的数据
  • CAS算法:乐观锁的一种实现,可以建超一部分不必要的锁竞争带来的上下文切换
  • 根据实际使用线程数:尽量使用少的线程数,如需求不需要创建很多的线程还是创建了,这样会导致大量的线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2、线程安全

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。

形成死锁的几个必要条件

  • 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。
  • 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  • 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A 在等 B,B 在等 C,C 在等 A)

如何避免死锁

  • 避免一个线程同时获得多个锁;
  • 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
  • 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

一些概念

同步和异步

同步,Synchronous,即调用方法开始,一旦调用就必须等待方法执行完返回才能继续下面的操作。 举个例子,你去银行ATM取钱,你必须等到ATM吐完钱你拿到钱取完卡你才能离开。

异步,Asynchronous,即不关心方法执行的过程,触发要调用的方法就继续执行下面的操作,不会像同步那样阻塞直要到方法完成才继续。举个例子,你这次要取钱,数量较大,你直接电话或者APP预约银行说你要取多少万现金,这段时间银行会为你准备钱,而这与你都没什么关系,然后你只要按预定的时候去取就行了,对你于而言,你们是触发了一个异步动作而已。

并发和并行

并发,Concurrency,即一段时间内多个任务在执行,但不一定是同时在执行,它们可能是交替在运行,也有可能是串行运行的。

并行,Parallelism,这个就是多个任务在同时执行,可以理解为并发里面有一部分任务在并行执行。

实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

阻塞和非阻塞

阻塞,Blocking,如果一个线程占用了一个公共资源而没有释放对它的锁,另外别的一些线程想要继续执行就只能等它释放锁,这时候就造成阻塞了。

非阻塞,Non-Blocking,就是没有阻塞,线程可以自由运行,没有锁定公共资源,不相互阻塞运行。

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。