Java Volatile变量

来源:互联网 发布:xampp linux 32位下载 编辑:程序博客网 时间:2024/06/05 18:36

Volatile是一种稍弱的同步机制,是一种轻量级的synchronized,相对于synchronized来说,它的开销比较小,功能也较弱,实现理想的线程安全,有一些条件限制。

引言

Volatile作用于共享变量(可以被多个线程共享的变量,包括所有的实例变量,静态变量,数组元素,他们都被存在堆中)。变量被声明为Volatile类型后,编译器和运行时都会注意到这个变量时共享的,所以不会将该变量与其他内存操作一起重排序。在没有同步的情况下,编译器以及运行时等都可能对操作的执行顺序进行一些意向不到的调整。

/** * @author gologo *摘自 java并发编程实战 */public class NoVisibility{private static boolean ready;private static int number;private static class ReaderThread extends Thread{public void run() {while (!ready) {Thread.yield();System.out.println(number);}}}public static void main(String[] args) throws InterruptedException {new ReaderThread().start();//主线程sleep 1ms 保证ReaderThread的运行Thread.sleep(1);number = 42;ready = true;}}
主线程sleep 1ms是我添加的,不然ready为true,使得ReaderThread的run方法直接结束。睡眠1ms后,发现ReaderThread打出来的最后一个数,时而为42,时而为0。原书甚至说该循环可能一直执行下去,可能永远读不到ready的值(这是一个极端情况啦)。指令重排序在程序运行中是比较普遍的,编译器或者运行时为了提高效率和性能,就会根据程序局部性原理对指令的执行顺序进行重排,所以,很有可能是先写ready,再写number。对于读线程来说,看到ready和number 的顺序也是不知道的。如果先获得number,42就会被打出来,如果先获得ready,只有0会打出来。所以, 多线程环境中,在没有同步的情况下,试图判断对内存的执行顺序,往往是一种徒劳。

JVM变量内存模型

jvm运行时分配内存时,会有一个内存区域是jvm虚拟机栈,每个线程都有自己的线程栈,线程栈保存了运行时变量信息。但线程访问某一个对象值的时候,会在本地建立该对象值的副本,之后线程就不再与堆内存的变量打交道,而是直接操作本地副本,然后在某一未知的时刻(线程退出之前),将变量的值再写回到堆内存中。所以,很容易出现不可见的情况。所谓不可见,就是说某个线程对共享变量值更改后,其他线程并没有觉察到值的变化,读到的是之前失效的数据,这种值的改变对于其他线程来说就是不可见的。如果变量被声明为Volatile,情况就大不一样了,java线程内存模型能确保所有线程看到这个变量的值都是一致的(可见性)。简单的说:当某个线程更改共享变量值后,会立即写回到堆内存,其他线程再访问该变量时,本地副本就会失效,然后重新从堆内存中获取变量值建立本地副本。所以Volatile变量读写开销要比普通变量大,现在Volatile变量读开销基本可以比拟普通变量,而写的开销要远大于普通变量(大致要完成两点:1、其数据必须写会到堆内;2、其他线程访问副本失效)。

Volatile变量使用条件

我们知道,同步必须满足两个条件:1、原子性, 2、可见性。而Volatile变量只能保证可见性。如果在多线程中对Volatile变量进行非原子性的操作,很容易破坏线程安全。比如Volatile int count;然后执行count++操作,该操作大致可以分为3个原子步骤,read count —> add count,1 —>write count;所以多个线程读写count时,很容易读到脏数据。如果能保证对Volatile变量的操作是原子性,基本就可用于同步中了。具体来说,要是Volatile变量提供理想的线程安全,必须同时满足:

1、对变量的写入操作不依赖于当前值,或者你能确保只有单个线程更新变量的值。

2、该变量不会与其他状态状态一起纳入不变形条件中。

3、在访问变量时不需要加锁。

Volatile变量能够在程序中保持相当的独立性。

Volatile变量典型用法

1、状态标识:指示一次重要的事件,初始化完成或者循环退出等等。

volatile boolean asleep;....while (!asleep) {countSomeSheep();}

当asleep被其他线程修改的时候,执行判断的程序能够发现该值的变化,做出正确的判断。否则,如果不能保证可见性的话,就会发生引言中程序的情况。

2、发布不可变状态:不可变对象,final域能确保在初始化过程中的安全性,在某些情况下,不可变对象能提供一种弱形式的原子性。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就不必担心另一个线程会修改对象的状态。如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。

@Immutable  class OneValueCache {     private final BigInteger lastNumber;     private final BigInteger[] lastFactors;      public OneValueCache(BigInteger i,                          BigInteger[] factors) {         lastNumber  = i;         lastFactors = Arrays.copyOf(factors, factors.length);     }      public BigInteger[] getFactors(BigInteger i) {         if (lastNumber == null || !lastNumber.equals(i))             return null;         else             return Arrays.copyOf(lastFactors, lastFactors.length);     }  } 
@ThreadSafe  public class VolatileCachedFactorizer implements Servlet {     private volatile OneValueCache cache =         new OneValueCache(null, null);      public void service(ServletRequest req, ServletResponse resp) {         BigInteger i = extractFromRequest(req);         BigInteger[] factors = cache.getFactors(i);         if (factors == null) {             factors = factor(i);             cache = new OneValueCache(i, factors);         }         encodeIntoResponse(resp, factors);     }  } 
OneValueCache用来保存缓存的数值及其因数,声明为Volatile时,当cache设置为引用一个新的OneValueCache时,其他线程可以立即看到新缓存的数据。与cache相关的操作不会互相干扰,因为OneValueCache是不可变的,并且每条相应的代码路径只会访问它一次。通过使用包括多个状态变量的容器对象来维持不变性条件,并使用一个Volatile类型引用来确保可见性,使得VolatileCacheFactorizer在没有显式使用锁的情况下仍然线程安全的。

3、独立观察:如果一直有一个线程在更改变量的值,而其他线程都是读取该变量的值。

4、与synchronized实现开销更低的读写锁:我们知道Volatile变量在写的时候不一定能保证原子性,所以在写入的时候,加一个锁。这样能够保证一写多读的操作,而如果仅仅是加锁的话,只能一个线程访问该值。

更多参考:

1、http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

2、http://www.infoq.com/cn/articles/ftf-java-volatile

3、http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

4、http://sakyone.iteye.com/blog/668091

0 0
原创粉丝点击