Java并发(一)

by:leotse

并发,并发,并发

有顺序编程,就有并发编程。事实上,几乎我们所有的程序都可以通过顺序编程来完成,只是你必须忍受一些非常情况。
并发说起来很奇怪,它具有可论证的确定性,但是实际上却具有不可确定性。它的不可确定性是因为你没有办法预知在实际情况下会发生事情导致工作失败,而且你也没有办法通过编写代码进行完备的测试。
虽然实际应用中,并发很难做到完全掌握,但是这不能成为我们不作为的理由。

如果视而不见,你就会遭其反噬。

作为一名骄傲的程序员,并发编程是基础,也是必备知识。

为什么需要并发

这是一个老生常谈的问题,但是我们不能规避这个问题,要想深入理解一件事物,就必须弄清楚它的来龙去脉。

我们设定我们现在在做饭,我们需要做的事情list如下:
1)淘洗大米;
2)插电做饭;
3)洗好紫菜;
4)打鸡蛋;
5)洗净豌豆并剥开;
6)牛肉切丁;

是的,我们想来一顿米饭+紫菜蛋汤+豌豆牛肉粒的穷人版商务套餐。我们可以有很多种方法完成这顿午餐。其中有两种常见可行的方案:
1.完全按照顺序来,即1) -> 2) -> 3) -> 4) -> 5) -> 6),也就是每次都需要等待上一个步骤完成再进行下一步;
2.我们将时间打碎,在完成1) -> 2)之后,无需等待2)完成,即不用等到生米煮成熟饭,就开始着手洗紫菜、打鸡蛋,然后在煲汤的同时,开始准备豌豆和牛肉。

很明显,除了死心眼,大家都会选择第2种方案或者其他类似的方案。第1种方案就是我们所说的顺序编程,而第二种方案就是并发编程。

通常来说,并发是提高运行在单核上的程序的性能。

为什么是提供单核上程序的性能呢?
如果只有一个核即处理器,我们来回切换工作,反而会增加上下文切换的损耗,怎么会是提高性能呢?如果和上述的例子结合起来就不难发现,如果有些事情需要等待,但是这个等待的时间间隙我们可以做一些并不依赖于正在处理的事情的其他业务,比如做菜并不依赖煮饭(除非你用同一口锅),洗豌豆并不依赖于煲汤。这样我们的时间就被充分利用起来了。用行话来说,就是阻塞。如果程序中的某个任务因为该程序控制范围外的某些条件而导致不能继续执行,那么我们就说这个任务或者线程阻塞了。如果没有引入并发,那么线程将一直阻塞,直至这些条件发生改变。

某种程度上讲,没有阻塞,就没有并发。(单核)

最简单直接的方式就是使用进程。
多任务操作系统会周期性将CPU从一个进程切换到另一个进程,虽然会有切换带来的耗时,但是对用户来说基本可以当作同时在运行多个进程。而且操作系统会帮我们将这些进程隔离开来,每个进程都有自己的一亩三分地,使得它们变得相互独立。这对并发编程来说,似乎非常理想,不会出现争夺公共资源的情况,每个任务都在自己的地址空间中做着自己的事情,进程们也根本不需要相互通信。但是进程通常都有数量限制以及开销上的限制,因此在实际编程中并不常见直接将进程作为并发的实现方式。

函数型语言被设计为可以将并发任务彼此隔离,其中每个函数的调用都不会产生任何副作用,因此可以当作独立的任务来驱动。

在Java中,我们常常用到的是在顺序编程的基础上使用比进程更小粒度的线程来实现并发。编写Java并发程序的一个最基本的困难是如何协调不同线程驱动的任务之间对一些公共资源(如IO、内存)的使用。

使用并发的另一个目的是改进代码设计。我们前面提到,我们可以使用顺序编程完成几乎所有程序,但是这会使得我们的程序变得复杂,使用并发可以使得我们的程序结构更加清晰简化。

我们假设我们的程序由很多的部件的组成,每一个部件都可以独立运作,那么理想情况就是每一个部件都对应一个线程,但是实际上我们的多线程系统一般都会限制线程的数量,而且这个数字不会太大,因此我们往往我们不能达到理想情况下的每一个部件都对应一个线程。在Java中,线程机制是抢占式的,也就是说,系统可能周期性中断线程,然后切换到另一个线程,从而让每个线程都有运行的时间片。我们可以通过协作多线程来解决这个问题。在协作式系统中,每个任务都是可控的,即可以通过编程人员的控制自动放弃控制,相对于抢占式线程机制,协作式线程机制一方面上下文切换的开销小,另一方面同时执行的线程数量理论上没有限制。

当然,并发是有代价的,那就是我们需要额外花费上下文切换的代价,但是这个代价不算高昂,特别是和它所带来的收益进行比较。