一、定义
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赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。文摘:
如果给一个变量加上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的对象监视器,如果该钥匙没人拿走(之前没有线程操作该方法或操作完成),则当前线程拿走钥匙(获取对象监视器),并操作方法;当操作完方法后,将“钥匙”放回原处!
如果“钥匙”不在原处,则该线程需要等待别人把钥匙放回来(等待即进入阻塞状态);如果多个线程要获取该钥匙,则它们需要进行“竞争”(一般是根据线程的优先级进行竞争)
http://www.cnblogs.com/mengyan/archive/2012/08/22/2651575.html
附:
原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子:
请分析以下哪些操作是原子性操作:
1 x = 10; //语句1
2 y = x; //语句2
3 x++; //语句3
4 x = x + 1; //语句4
咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
https://my.oschina.net/wangnian/blog/668490