Java并发编程(一)并发的概念

并发是什么?


并发是什么?

顾名思义,同时发生,一起行动。单纯从Java上讲,并发是个一个thread类,它通过集成这个类,获得对应的api,提高程序的运行效率。不过这里暂时不讲和代码有关的事物,我们先为什么会诞生并发开始讲。

这里会引入一个新的词叫摩尔定律,“每隔18个月到24个月,计算机的性能会翻一倍”。这条定律持续了半个世纪之久,在此期间,计算机的能力也正如定律所描述的一样,不断的倍增。但是在几十年前,这条定律有点不管用了。你可以很简单的看作为,我们不再能够制造出性能更好的芯片,单核的能力已经很难再往上升了,所以在21世纪初,人们开始把多个芯片集成起来,企图用数量的增长去代替质量的不足,thread类,也正因此走进了程序员的视野。复杂的并发远远比单核的串行困难的多,所以,程序员的噩梦来了。

并发一定优于串行吗?

这里可以很直接的告诉大家,并发在大多数的情况下都优于串行。

举一个例子:

小明被告知明天要上台演讲,而小明今晚开始到明天要做的事情为

吃饭(半小时)—>洗澡(半小时)—>构思并撰写演讲稿(1.5+1.5=3小时)—>睡觉(8小时)

我们可以通过面向对象的编程写出代码,其预计的执行时间为12小时,而引入的并行这个方式,让小明可以边吃饭边思考,边洗澡边思考,让其撰写演讲稿的时间减少一小时

这样最后的执行时间就变成了11小时,但是呢,无论增加多少线程,都只能让小明的构思时间减少1.5小时,其吃饭洗澡睡觉的时间是无法被改变的,也就是说,极限时间为10.5小时。

从上面的例子可以得出,并行在绝大多数的情况下都优于串行。少数情况下和串行是没有区别的。这里先不谈特殊情况下出现的并行比串行更差劲的事例,因为这并没有太多意义。要记住,我们是面向对象编程

所以,学会怎么利用并行,在当今,显得是多么的重要。

这里不得不提到关于并行这个概念,所拥有的三大特性

三大特性

原子性

顾名思义,具有原子的特性,无法被分割。在程序中可以看作为,这个操作不可被中断,各个线程想要执行这个操作也得一个一个来。

可见性

可见性的意思就是,我们所执行的操作,是可见的。就像我们写一段代码

1
2
3
a=1;
a=2;
b=a;

程序就会把a赋值为1,b赋值为2。这看上去貌似很寻常,因为在串行的情况下运行时,可以永远保持可见性。

而在并行情况下运行时,却不一定可见,例如:

1
2
3
4
5
6
//线程1
i = 0;
i = 2;

//线程2
j = i;

在线程1和线程2一起运行之时,你并不能确定,此时的j到底取值是多少,因为此时的i是在两个线程之间共享的,它可以是0,也可以是2,这样对我们并不可见。但我们也可以深挖其中的原理。

可见性

先看图示,我们的在定义了值为0之后,就会被写入到主内存,而我们在多个线程取i的值的时候,首先会把i加入到本地的缓存当中,读取值就从缓存中读取,因为这样多个线程就不会总从主内存读取,从而提高读取的效率,但是我们在更改了i的值的时候因为有一个线程嗅探的机制,一旦改了i的值,比如a线程修改了i的值后,就会向所有的线程发出一个警告,告诉所有线程i的值已经被更改了,需要重新从主内存中读取。但是呢,发出这样一个信息也是需要时间的,线程b也会因为时间差的关系导致了不能及时获得最新的值。

有序性

这里会讲到一个新的概念,叫指令重排,意思就是程序在实际运行的时候,代码段运行的顺序会和你写的顺序不一样。

像下面这段代码,可能会出现一个问题,那就是先执行a=3再去执行a=1,这样会造成了最终结果不太一样,不过没关系,这种情况在串行条件下不会发生。

1
2
3
a=1;
b=2;
a=3;

但是,在并行条件下,指令重排是怎么发生的呢?

1
2
3
4
5
6
//线程一
i=1;
j=i;
k=i;
//线程二
j=2;

像这样,两边线程看似互不干扰,各自有各自的工作,但是我们看到了j和k都在读取同一个变量i,而i在这种情况下会被读取两次,而此时如果发生了指令重排,结果就会变成为

1
2
3
4
5
6
//线程一
i=1;
j=i;
k=j;
//线程二
j=2;

而如果线程二发生在j=i之后的话,会出现这样一个状况,线程二修改了j的值,然后线程一再读取了j的值,这样导致最后的结果k=2。你可以看到,其实如果一直按照它本来的写法,这段代码是不可能出问题了,而一旦指令重排出现了这样的问题,谁又能在几十万行代码中找出来呢?

你说为什么会出现指令重排呢?不发生重排不就没有这样的问题了吗?其实设计者的初衷很简单,一切都是为了提高效率

为什么这样能够提高效率?这里就要涉及到汇编语言的问题了。简而言之就是,读取和修改一个值需要分很多个步骤,如果将多个步骤一起进行的话会提高效率,从而导致的指令重排,汇编的机制这里暂且不讲。

那怎样才能解决呢,最简单的方法就是声明volatile关键字,如下:

1
volatile int i;

这样谁都不能够对i进行指令重排了,后面会讲到其他的办法,比如synchronized等。

这里仅仅是对并行这个概念做一个非常简单的介绍。