1.计算机内存模型的相关概念
计算机在执行程序时,每条指令都是在CPU中执行的,在指令的执行过程中,涉及到数据的读取和写入。由于程序在运行的过程中数据是放在"主存"中的,
由于数据从主存中读取数据和写入数据要比CPU执行指令的速度慢的多,如果任何时候对数据的操作都需要通过和主存进行交互,会大大降低指令的执行速度。
因此在CPU处理器里面有了高速缓存。
也就是,当程序的运行过程中,会将运算的需要的数据从主存复制一份到CPU的高速缓存中,那么当CPU进行计算时就可以直接从他的高速缓存读取数据
和向高速缓存写入数据,当运算之后将高速缓存中的数据刷新到主存中。
下面是计算机中,数据缓存通过总线、缓存一致性协议在处理器CPU和内存之间的传递过程:
2.缓存不一致问题解决
举个例子说明:
例如
int i= 0;
i=i+1;
这段代码在计算机中是如何计算的。
当线程执行到这个语句的时候,会从主存中读取数据i的值,然后复制一份到高速缓存中,然后CPU指令对i进行+1操作,然后将数据写入到高速缓存中,最后将高速缓存中的i最新的值刷新到主存中。
这个代码在单线程中运行是没有问题的,但是在多线程中运行就有问题了。在多核的CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存。
假如有两个线程A,B;初始的时候分别从主存中读取i的值,然后放在各自所在的CPU高速缓存中,然后线程A进行+1操作,然后把i最新的值写入到主存。此时线程B的高速缓存中i的值还是0,进行+1操作,i的值为1.然后线程B把i的值写入到内存。最终i的值是1,而不是2.
这就是缓存一致性问题。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
这2种方式都是硬件层面上提供的方式。
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LOCK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
3.深入剖析volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层含义
volatile关键字的作用:
* 可见性:可以保证不同线程对这个变量的可见性,一旦某个线程修改了volatile变量的值,这个值对其他线程是可见的。
* 原子性:对单个volatile的读和写具有原子性,但是对volatile++这种复合的计数操作不具有原子性。不能用于线程安全计数器。 因为volatile++这种操作,实质 上是由一个读取-修改-写入操作序列组成的组合操作。
看下面的代码:
package concurrentMy.Volatiles; public class VolatileFeaturesExample { int a = 0; volatile boolean flag = true; public void writer(){ a = 1; //1 flag = true; //2 } public void reader(){ if(flag){ //3 int i= a; //4 System.out.println(i); } } }
假设线程A执行writer方法后,线程B执行reader方法。
(1)从happens-before原则上来讲,对volatile的写操作一定happen-before对volatile的读。也就是说上述代码2 happens-before与3,根据程序的执行顺序1 happens-before 2,3 happens-before 4。根据happens-before的传递性,1 happens-before 4.也就保证了线程A,写入volatile flag 变量,立即对B线程可见。
(2)从JMM内存语义上来讲,当写一个volatile变量的时候,JMM会把改线程的对应的本地缓存中的共享变量值立即刷新到朱内存中。当读一个volatile共享变量时候,JMM会把该线程对应的本地内存置为无效,也就是上面缓存一致性说的会把该CPU线程对应的缓存行至为无效。直接从主存中读取。
下图为线程A执行volatile flag写后,共享变量的状态示意图:
从写-读的内存语义上来讲,一个线程把共享的volatile写入线程本地内存,然后在刷新到主内存,然后其他线程从主内存中
读取这个共享变量。这样其实就实现了线程之间的通信,通过主内存。
(1)线程A写一个volatile变量,实质上是线程A向将要读这个volatile变量的某个线程发出了消息。
(2)线程A写一个volatile变量,然后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
3.底层实现
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
4)编译器不会对volatile变量的读和读后面的任意内存操作重排序;编译器不会对volatile变量写和写前面任意内存操作做重排序。
4.锁和volatile关键字的对比
功能上锁比volatile更强大,可以保证操作的原子;而可伸缩性和执行的性能上volatile比锁更有优势。
volatile可以看成一种"程度较轻的synchronized",与synchronized 块相比,volatile变量的使用所需的编码较少,并且运行开销比较小。
但是不能保证原子性,需要结合一些技术来保证,比如CAS。并发包下面的原子类,可重入锁的实现就是通过volatile结合CAS来实现的。
5.开销较低的读-写锁策略:
之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本例中的写操作违反了使用 volatile 的第一个条件,
因此不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有
变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代
码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,
结合这两个竞争的同步机制将变得非常困难。
6.下面看一组例子:
多线程对共享变量++操作,单使用volatile变量的话,会出现线程安全的问题,会导致计数不对,下面通过几种方法实现计数功能:
(1)加ReentrantLock互斥锁:
package concurrentMy.Volatiles; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2016年4月8日 下午6:07:36 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class VolatiteLock implements Runnable{ // 不能保证原子性,如果不加synchronized的话 private volatile int inc = 0; Lock lock = new ReentrantLock(); /** * * 理解:高速缓存 - 主存 * 通过ReentrantLock保证原子性:读主存,在高速缓存中计算得到+1后的值,写回主存 * (方法说明描述) * */ public void increase() { lock.lock(); try { inc++; } catch (Exception e) { e.printStackTrace(); }finally{ lock.unlock(); } } public void run() { for (int i = 0; i < 10000; i++) { increase(); } } public static void main(String[] args) throws InterruptedException { VolatiteLock v = new VolatiteLock(); // 线程1 Thread t1 = new Thread(v); // 线程2 Thread t2 = new Thread(v); t1.start(); t2.start(); // for(int i=0;i<100;i++){ // System.out.println(i); // } System.out.println(Thread.activeCount() + Thread.currentThread().getId() + Thread.currentThread().getName()); while (Thread.activeCount() > 1) // 保证前面的线程都执行完 Thread.yield(); //20000 System.out.println(v.inc); } }
(1)使用原子类,原子自增操作,其底层实现,通过volatile+Cas保证原子性操作,保证读-改-写操作顺序执行,不会发生线程安全的问题。
package concurrentMy.Volatiles; import java.util.concurrent.atomic.AtomicInteger; /** * * * * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年7月14日 下午3:58:30 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class VolatileAtomic implements Runnable { private AtomicInteger ai = new AtomicInteger(0); /** * atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。 * (方法说明描述) * */ public void increaseAtomic() { ai.incrementAndGet(); } public void run() { for (int i = 0; i < 10000; i++) { increaseAtomic(); } } public static void main(String[] args) throws InterruptedException { VolatileAtomic v = new VolatileAtomic(); // 线程1 Thread t1 = new Thread(v); // 线程2 Thread t2 = new Thread(v); t1.start(); t2.start(); // for(int i=0;i<100;i++){ // System.out.println(i); // } System.out.println(Thread.activeCount() + Thread.currentThread().getId() + Thread.currentThread().getName()); while (Thread.activeCount() > 1) // 保证前面的线程都执行完 Thread.yield(); System.out.println(v.ai); } }
参考文章:1.http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
2.http://www.cnblogs.com/dolphin0520/p/3920373.html
3.http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-frame.html
4 深入浅出 Java Concurrency (5): 原子操作 part 4:http://www.blogjava.net/xylz/archive/2010/07/04/325206.html