原子性与可见性

一、定义

1.可见性

在多核处理器中,如果多个线程对一个变量(假设)进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主存通信的次数);同样在单核处理器中这样由于“备份”造成的问题同样存在!

这样的优化带来的问题之一是变量可见性——如果线程t1与线程t2分别被安排在了不同的处理器上面,那么t1与t2对于变量A的修改时相互不可见,如果t1给A赋值,然后t2又赋新值,那么t2的操作就将t1的操作覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。

2.原子性:

众所周知,原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;

由不可分性可知,原子性是拒绝多线程操作的(只有分解为多步操作,多个线程才能对其操作:就像一个盒子里有多个兵乓球,多个人能够从盒子里拿乒乓球;如果盒子只有一个兵乓球,一个人拿的话,其他人就拿不到了;这就是原子性,乒乓球就具有原子性,人就相当于原子)

 简而言之——不被线程调度器中断的操作,如:

赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性

原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作!

3.非原子性操作

类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:

(1)取出a和b

(2)计算a+b

(3)将计算结果写入内存

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。

类似的,像"a++"这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作

4.原子性与可见性的关系

原子性与可见性并没有直接关联的关系。说道这里,不得不要讨论一下多线程带来的问题及其本质。

(1)先来点废话,有可能会将多核与单核处理器进行不同的区分,这里我搞混了,其实在代码级别来说它们是相同的!

单核机器的多线程其实是为每个线程分配一个时间片段,所以实际上这些线程在微观来说在一个时间段内只有一个在执行。这里产生的问题是如果一个线程操作一个内存空间然后突然被线程调度器终止掉(挂起),由另一个线程获取CPU时间来对这个空间进行操作,那么着之间会产生不可预知的问题。

多核机器的基本原理与此是相同的,不同的是在同一时间,可能会有多个线程同时在进行操作(因为每个核心都可运行一项操作)。前面讲到,多核机器由于多核的原因其多个线程对于相同内存的操作会产生可见性的问题。(可见性在单核和多核中同样都存在)

(2)多线程中可见性造成的问题:

多个线程对相同变量的修改相互不可见,导致某部分操作被覆盖,比如:

count++; t1与t2两个线程准备操作它,当t1在自己存储空间内修改完count值之后,并没有及时将count修改回去,而是执行了count其它的操作——这时候,t2开始执行该操作,但是它并没有发现count值进行了改变,这样就造成了count值没有被及时更新而产生的相关错误。

(3)其它问题:

同样是count++语句,产生问题的语句还可能是其它原因造成的:t1与t2执行该语句,t1只比t2稍慢一点,t2修改后count,t1又将自己的结果写入count,这样t1的结果会对t2的结果进行覆盖,这种覆盖会造成一项不到的错误。

(1.2)非原子性造成的问题,多个线程在执行动作时某一方的“动作”“覆盖”了另一方;

(5)讨论:

可见性的问题造成了多线程的问题的一部分,确定变量的可见性只能解决一部分多线程的问题;而操作原子性是解决多线程的总的方法,因为它拒绝多个线程在同一时刻操作相同的一段内存。

 

5.volatile与synchronized关键字

(1)volatile

volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。文摘:

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量
的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示
VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟C中的一样 禁止编译器进行
优化~~~~

注意:

如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对long和double的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!

参考链接: http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

(2)synchronized

synchronized为一段操作或内存进行加锁,它具有互斥性。当线程要操作被synchronized修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。

简单的理解方法:

synchronized(object) method();

这相当与为menthod()加了一把锁,这把锁就是object对象;当线程要访问method方法时,需要获取钥匙:object的对象监视器,如果该钥匙没人拿走(之前没有线程操作该方法或操作完成),则当前线程拿走钥匙(获取对象监视器),并操作方法;当操作完方法后,将“钥匙”放回原处!

如果“钥匙”不在原处,则该线程需要等待别人把钥匙放回来(等待即进入阻塞状态);如果多个线程要获取该钥匙,则它们需要进行“竞争”(一般是根据线程的优先级进行竞争)

 

 

 

posted @ 2012-08-22 22:30  stopit  阅读(13625)  评论(3编辑  收藏  举报