并发,one
引言
最近工作当中写了一个有关并发的程序,引起了LZ对并发的强烈兴趣。这一下一发不可收拾,LZ用了一个多星期,看完了这本共280+页的并发编程书。之所以能看这么快,其实这主要归功于,自己之前对并发就有一定的理解。在这种前提下看书,其实只是一个印证自己之前想法的过程而已,因此看起来会比较快,而且在看的时候,会有多次这种感觉,“擦,原来还真是这样的”。
尽管LZ已经说了看书看的快的原因,但不管怎么说,书看的太快,肯定难免有遗漏。因此博客此时就派上用场了,它绝对可以帮你查缺补漏。因为在写的过程中,你会发现,之前你读的时候自以为理解的透透的东西,却无法给别人讲清楚。这就说明,你需要补补漏了。
并发的来源
并发的由来,从现在来看,似乎是必然的。因为人是一种懒惰而又急躁的动物,这是人的本性。因为急躁,并发就出现了,因为懒惰,多核时代就来了。
为什么这么说呢?
如果所有的程序都是串行的(之所以说如果,是因为LZ接触到电脑时,它已经是并发的了,因此只能想象一下),那么当你打开了一个特别慢的网页,最后你等不及想关掉浏览器的时候,你会发现,你必须等加载这个网页的事做完你才能关闭它。这是何其蛋疼的一件事。急躁的人们能允许这种事发生?因此并发就出现了。
再来想象一下,如果我们想让一个程序运行的更快,从直觉上讲,我们应该让CPU运算的更快。比如以前一秒可以计算1000次,现在我们让它在一秒内可以计算10000次。但是懒惰的人们发现,这种直觉上的方式似乎非常困难,想要硬生生的提高CPU的速度(缩短时钟周期)是非常困难的。因此懒惰的人们就想到一种偷懒的办法,一个CPU一秒可以计算1000次,两个的话不就可以计算2000次了(实际并非如此,但我们可以这么理解多个CPU带来的效率增加),这个相对简单的办法最终被人们采用了。因此多核时代就到来了。
并发的危险
说起并发引起的危险,在LZ的理解来看,主要来源于程序所给人带来的直觉造成的误导。这一点在LZ所写的计算机系统原理中有讲到(可以将这两本书的内容联系起来),比如下面这个程序,它给人的直觉是,a首先变成了1,然后b变成了2。
int a = 1;
int b = 2;
直觉是这样的,但往往是错误的,因为在程序真正执行的时候,可能是b先变成了2,a又变成了1,更奇葩的是,很可能a和b都始终是0。估计说起两者的赋值顺序颠倒,各位还可以理解,但是说到两者可能都是0,有的猿友就懵了,有种瞬间被颠覆三观的感觉,但是学习并发往往就是颠覆你三观的过程。
这种直觉与现实之间的不同,就给并发的程序造成了危险。它可能引起你预料之外的错误,而且往往是防不胜防。因此并发是诱人的,但也同样是危险的,一个诱人的东西永远都伴随着危险,就像高贵的玫瑰往往都是有刺的。
知道了上面这些,我们就可以来看看安全性和活跃性了。安全性是指,“程序不会出现糟糕的事情”,活跃性则是指,“好的事情一定会发生”。可以看出来,安全性更多的是在强调执行的程序是正确的,而活跃性更多的是在强调程序可以正确的往下进行(有点绕?那就对了)。
举个例子,对于一个并发递归求解的程序来讲,安全性则可以保证结果的正确性,而活跃性则可以保证这个程序总能得到一个正确的结果或者最终发现它没有解而抛出无解的异常。
Java的并发
对于大部分从事Java开发不久的程序猿来讲(包括LZ),并发一般都是很少接触到的(这里主要以LZ的领域来说,即J2EE),因为现有的框架已经将很多并发的问题给解决了,并给我们这些无脑程序猿创造了一个串行程序的环境。
比如,在J2EE领域的servlet规范当中,servlet是单例的,并且非常有可能,甚至可以说一定会被多个线程并发的去访问。因此servlet其实是有并发的安全性问题的,除非你不在servlet当中记录任何状态。但是当前比较火的MVC框架struts2已经帮我们解决了这个问题,尽管Action当中经常会有一些数据或者说状态,但Action在struts2中是非单例的,这相当于每个Action实例都是线程私有的,因此不存在并发问题。
有一些情况下,我们可能会接触到并发问题,比如,你需要做一个单例的对象,那么这个对象一般可以被全局访问,因此就可能存在并发的问题。可以这么说,几乎所有采用了单例模式的对象都会涉及到并发的问题,除非这个对象没有任何状态,但是这往往不会出现,因为没有状态的单例对象是没有意义的,它们更好的处理方式应该是一个无实例(即将构造函数私有化)且充满了静态方法的类。
很多程序猿在初次意识到并发时,都会采取一个看似万能却并非一定有用的方法,那就是将一个类的所有方法加上synchronized关键字。LZ以前也是这样的,而且还自认为十分高端,现在想想,LZ实在自惭形秽。当时LZ对synchronized的理解,就知道它可以让很多线程一个一个来执行这个方法,至于其它的特性,就不太明白了。
其实在大部分时候,一些比较简单的场景中,上面这种无脑做法还是能起到相应的作用的,也就是说,它可以保证安全性与活跃性。但是另外一个特性就无法保证了,那就是性能。无脑的方法同步,有时候会将性能降低数个数量级,可能会使很多线程都在等待,CPU却一直处于1%利用率的情况。
通常情况下,对于安全性、活跃性以及性能来说,我们会将性能放在最后一位,引用之前看过的一句经典的话,是用来形容面向对象设计的,即“可复用的前提是可用”。同样的,对于并发的程序来讲,“性能高的前提是程序的执行是正确的”。
小结
今天就暂且写这么多吧,对于并发,其实想说的还有很多,毕竟刚看完这本书。后面还会陆续给出自己的理解,但是会穿插着《计算机系统原理系列》的内容,这个系列也该继续前进了,因为并发已经耽误了它太久。