标签:
两条规定 :
线程对共享变量的所有操作都必须在自己的 工作内存 ( working memory,是cache和寄存器的一个抽象,而并不是内存中的某个部分, 这 个解释源于《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有不少人觉得working memory是内存的某个部分,这可能是有些译作将working memory译为工作内存的缘故,为避免混淆,这里称其为工作存储,每个线程都有自己的工作存储 )中进行,不能直接从相互内存中读写不同线程之间无法直接访问其他线程工作内存中的变量, 线程间变量值得传递需要通过主内存来完成。
导致共享变量在线程间不可见的原因主要有指令重排序与线程交错执行;
1.编译器优化的重排序(编译器优化)
2.指令级并行重排序(处理器优化)
3.内存系统的重排序(处理器优化)
例如:
·as-if-serial语义:无论怎样重排序,程序实际执行的结果应该与代码书写顺序执行的结果一致 ( Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义 )
例如:
在单线程中,第一行与第二行代码可以重排序,但第三行依赖于前两行,所以不能重排序,否则就会造成执行结果错误。重排序不会给单线程带来内存可见性问题,但在多线程中程序交错执行,重排序可能会造成内存可见性问题。
下面我们利用一个例子来分析指令重排序与线程交叉执行对内存可见性的影响:
3、重排序对多线程的影响
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
}
从上述两幅图中也可以发现线程交叉执行也是造成内存不可见的原因之一,如操作1和2是原子操作,且操作3和4是原子操作,那么程序会在启动线程A时,执行完线程A(操作1和操作2),才会执行线程B,并且在单线程中,重排序必须满足是as-if-serial语义,是不会改变执行结果的,这样就可以防止内存不可见。所以解决内存不可见的根本就是保持操作原子性。保证原子性的方法:synchronized关键字、ReentrantLock可传入锁对象、AtomicInterger对象。
总结,导致共享变量在线程间不可见的原因 :
1.线程的交叉执行(解决方案:保证原子性 ,例如使用synchronized关键字)
2.重排序结合线程交叉执行( 原子性 )
3.共享变量更新后的值没有在工作内存与主内存间及时更新( 可见性 )
需要注意的是,一般在Java运行过程中,执行引擎会尽量揣摩用户的意图,所以很多时候都会看到正确的结果,但是哪怕只有一次不可预期的结果出现影响也是非常大的,所以,在需要内存可见性的时候,我们一定要保证线程的安全。
Java语言层面支持的可见性实现方式 :synchronized、volatile、 final也可以保证内存可见性。
大多数情况下,我们认为synchronized可以实现互斥锁(原子性),即线程间同步。但很多人都忽略其内存可见性这一特性。
synchronized实现可见性原理,JMM关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中。
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从内存中重新读取最新的值( 注意:加锁与解锁需要是同一把锁 )。这样,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。
例子:通过多线程操作数字自增;
package testThread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VolatileDemo {
//private Lock lock = new ReentrantLock();
private volatile int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
/*try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
this.number++;
/*lock.lock();
try {
this.number++;//锁内执行程序可能会发生异常,为了保证锁能够释放,所以写在finally中;
} finally {
lock.unlock();
}*/
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
public synchronized void increase(){
this.number++;
/*synchronized (this) {this.number++;}*/}
1. 能够保证volatile变量的可见性(原理与synchronized关键字原理差不多)。当对 volatile变量执行读操作时,会在读操作前加入一条load屏障指令。当对volatile变量执行写操作时,会在写操作后加入一条store屏障指令;深入来说,通过加入内存屏障和禁止重排序优化来实现内存可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
2. 不能保证volatile变量复合操作的原子性。如上述例子中,我们可否在num前面加volatile 达到内存可见性呢? 答案是否定的,volatile实现共享变量内存可见性有一个条件,就是对共享变量的操作必须具有原子性。比如 num = 10; 这个操作具有原子性,但是 num++ 或者num--由3步组成,并不具有原子性,所以是不行的。
假如num=5,此时有线程A从主内存中获取num的值,并执行++,但在还未见修改写入主内存中,又有线程B取得num的值,对其进行++操作,造成丢失修改,明明执行了2次++,num的值却只增加了1.
3.volatile适用场合
在多线程中安全的使用volatile变量必须同时满足两个条件:
①对变量的写入操作不依赖其当前值,如number++不可以,boolean变量可以;操作本身必须是原子性的才能保证内存可见性;
②该变量没有包含在具有其他变量的不变式中,如果有多个volatile变量,则每个volatile变量必须独立于其他的volatile变量。
但大多数实际应用中,都会涉及到上述两个条件,所以volatile的使用时很少的。
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程,效率更高,从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁。
synchronized技能保证可见性,又能保证原子性,而volatile只能保证可见性,不能保证原子性。 如果能用volatile解决问题,还是应尽量使用volatile,因为它的效率更高 。
一个需要注意的点:
问:即使没有保证可见性的措施,很多时候共享变量一人能够在主内存和工作内存见得到及时的更新?
答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快的刷新缓存,所以一般情况下很难看到这种问题,而且也 与硬件性能有很大的关系,所以,结果都是不可预测的,正是因为不可预测,所以我们才要保证线程的安全问题。
另:java中long、double是64位的,其读写会分成两次32位的操作,并不是原子操作,但很多商用虚拟机都进行了优化,所以,了解即可 。
标签:
原文地址:http://blog.csdn.net/beatrice_g20/article/details/51628829