java并发之volatile关键字(TIJ21-21.3.3 21.3.4)

** 1 简介
    volatile是java中的一个保留关键字,它在英语中的含义是易变的,不稳定的。volatile像final、static等其他修饰符 一样,可以修饰class中的域,而不能修饰方法中的局部变量。当修饰class中的域时,volatile可以修饰primative类型或者任意对 象。下面这个例子展示了这一点:

public class TIJ_volatile {
    private volatile int i;
    private volatile String s;
    private volatile Object o = new Object();
    private volatile Object o2;
    public static void main(String... args) {
        volatile int i2 = 1;
    }
}

编译器会在main方法的volatile int i2 = 1 这一行给出错误警报。

** 2 一个例子看懂volatile域与普通域的区别
    那么,声明为volatile的变量与普通域变量有什么区别呢?从字面意思理解,相对于那么未声明为volatile的变量,volatile变量是更加不稳定的,更加易变的。
一个典型的需要不同的线程读写简单变量的例子,是允许一个线程通知另一个线程终止的“停止请求”标志位,在这个例子中volatile是非常好的选择:

public class StoppableTask extends Thread {
  private volatile boolean pleaseStop;  //pleaseStop变量存储在java堆中, 在new StoppableTask的时候。
  public void run() {
    while (!pleaseStop) {
      // do some stuff...
    }
  }

public void tellMeToStop() {
    pleaseStop = true;
  }
}

在上面的例子中,如果这个“停止请求”标志位没有被声明为volatile,同时又没有使用其他的同步机制,那么StoppableTask线程会从主内存中读取一次pleaseStop变量,并把它缓存在自己的线程栈(工作内存)中(在一些文章中,工作内存被看做是jvm对物理机器中的寄存器或者高速缓存的一种虚拟或者抽象,因此它的读写速度远高于主内存,从工作内存中读取数据可以提高读取速度,优化性能)。在后面的循环中,pleaseStop将不会被再次从主内存中读取,因此while循环将永远执行下去,程序失败。这就是非volatile变量的读取和写入方式。

当声明一个变量为volatile变量时,就等于告诉编译器,这个变量的值是非常易变的,所以不能像普通的变量那样,为了优化性能,对它的读取都从工作内存中进行,而不与主内存同步:因为如果不同步的话,volatile的值又是“易变的”,那么读取的工作内存中缓存的值有可能不是最新的,由此可能引发程序的执行失败!因为volatile值的工作内存中的缓存值可以与主内存的最新值及时同步,所以它的值永远看起来就像是直接从主内存中读取的,也永远像是直接往主内存中写入的。

相信到这里,大家应该明白了volatile的含义了吧?需要特别注意的是,volatile的这种语义直到java 5才得以完全正确地实现。在jva5以前的版本中,volatile并不能正常地工作。 在java5及其后续版本中,对volatile变量的读取看起来就像是直接操作主内存一样:它使得所有缓存在线程工作内存中的该变量的拷贝可以有效地与主内存中的变量同步。(volatile变量在不同线程中仍然有不同的拷贝)。

** 3 java内存模型(jmm)与可见性

那么什么是主内存(main memory)呢?什么又是工作内存呢? 这就不可避免地涉及到了java内存模型(jmm,java memory model)。jmm将会在本博客中专门用一篇文章来阐述,在这里我们只需要看一下下面的这幅流传深远的jmm示意图:

从上图可以看出,在jvm中,每个线程都有一个自己的工作内存(Working Memory),线程总是对主内存(Main Memory)中的变量进行备份,并存储在自己的工作内存中,并且对变量的读写都是在工作内存中进行的(图中的Load/Save过程)。正常情况下,线程在第一次从主存中读取该变量后,就假设该变量是相对稳定的、不易变的,因此当该线程以后再用到该变量,就假设自己工作内存中的备份与主内存中的变量是一致的,因此它会直接读取自己工作内存中的备份,这样做的主要目的是提高性能,因为从内存读取数据相对于高速缓存读取甚至是寄存器读取,是要慢很多的。

volatile变量的作用,就是要告诉线程:该变量是不稳定的,非常容易变化,因此每次读取工作内存中的该数据的备份,都要从主内存中重新load一遍。注意这样做相对于普通变量而言,会降低性能。讲到这里,其实可见性的含义也已经出来了(下面这段来源于网络):

相对于内存,CPU的速度是极高的,如果CPU需要存取数据时都直接与内存打交道,在存取过程中,CPU将一直空闲,这是一种
极大的浪费,所以,现代的CPU里都有很多寄存器,多级cache,他们比内存的存取速度高多了。某个线程执行时,内存中的一份数
据,会存在于该线程的工作存储中(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修改了变量a的值什么时候对线程2可见?此外,编译器或运行时为了效
率可以在允许的时候对指令进行重排序,重排序后的执行顺序就与代码不一致了,这样线程2读取某个变量的时候线程1可能还没有进行写入操作呢,虽然代码顺序
上写操作是在前面的。

根据上述描述我们不难看出,可见性其实指的是某一线程对主内存中的数据的修改,对其他线程而言是不是可见。

** 4 与synchronization等锁机制的区别

volatile关键字仅提供了synchronization提供的部分同步功能,也就是说,volatile保证了主内存中数据对所有线程的可见性,或者说提供了主内存与工作内存之间的同步功能。 但是,相对于synchronized关键字在java server端编程中的广泛应用,volatile的使用率相对来说是比较低的,因为它并不能解决所有可以由synchronized关键字解决的同步问题。

从下面这个例子可以看出,volatile并不能保证线程同时写变量所带来的同步问题:

public class Counter {
     public volatile static int count = 0;
     public static void inc() {
         //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
         count++; // 在java中不是原子操作。可以使用AtomicInteger保证原子性。
    }
     public static void main(String[] args) {
         //同时启动1000个线程,去进行i++计算,看看实际结果
         for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
         //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

上述程序在多线程环境下,大多时候会失败。 究其原因,程序中对count变量使用了volatile修饰符,保证了不同线程读取count的值时(在工作内存中读取),count值都与主内存中的值是一致的。但是这种一致性并不能保证两个线程同时执行count++时,count的值只增加了一次。为什么会这样的,原因在于在java中,count++操作并不是原子操作(注意在c++中count++是原子操作)。 count++本质上是以下原子操作的复合体: 从主内存中读取(load) count的值到线程的工作内存,在工作内存中对count的副本执行count++操作,把工作内存中的count值(更新后的)写入(save)到主内存。

在两个线程同时执行count++操作时,有可能出现以下情况:

假设主内存中count的当前值为0, thread 1 读取该值到工作内存中(0),此时线程调度机制中断了线程1,thread 2开始执行并读取该值到它的工作内存(0),此时线程调度机制再次中断了thread 2,thread1重新执行,并执行count++操作(针对工作内存中的count副本),此时即使重新从主内存中同步count,该值仍然为0,所以工作内存中的count值在执行count++操作后为1.此时线程调度机制再次中断thread1,thread2重新执行,并执行count++操作,因为此时thread1工作内存中的count值(1)并未save到主内存,所以thread2工作内存中的count副本在执行count++操作后,值更新为1.

在后续的程序执行中,不论thread1 thread2以何种顺序执行,最终主内存中的count值都为1,程序执行失败。

为了让该程序正确执行,有两种修改的方法,第一种是通过synchronized关键字使用内置锁(或者通过lock对象使用显式锁);第二种方法是使用在java5中引入的Atomic原子类。(原子类会在本文后面介绍到)。

** 5 原子性与原子类
    在上一节的例子中,已经涉及到了原子性的概念。原子性可以保证操作未完成前,不会被线程调度机制中断。也就是说,原子操作是保证在该操作完成前,不会被线程调度机制中断的操作。 需要注意的是原子性并不能保证并发程序的正确性:

public class TIJ_21_3_3_atomicityTest implements Runnable {

private int i = 0;

public int getValue() { return i; }

private synchronized void evenIncrement() { i++; i++; } // evenIncrement不是原子性的,可以被线程调度机制中断

public void run() {

while(true) evenIncrement();

}

public static void main(String[] args) {

ExecutorService exec = Executors.newCachedThreadPool();

TIJ_21_3_3_atomicityTest at = new TIJ_21_3_3_atomicityTest();

exec.execute(at);

while(true) {

int val = at.getValue();

if(val % 2 != 0) {

System.out.println(val);

System.exit(0);

}

}

}

} /* Output: (Sample)

191583767

*///:~

在上面的例子中,虽然getValue()方法中的操作是原子操作,但这并不能保证程序的正确性。可见原子性跟正确的并发是两个完全不同的概念。

原子性介绍完了,那么,原子类又是什么呢?下面来看一下: 原子类的概念是在Java SE5中引入的。典型的原子类包括:Atomiclnteger, AtomicLong, AtomicReference,等. 它们提供了以下形式的原子性条件更新操作: boolean compareAndSet(expectedValue, updateValue);以及原子性的修改操作:

int addAndSet(int delta)。看下面的例子:

//: concurrency/AtomicIntegerTest.java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;

public class AtomicIntegerTest implements Runnable {
  private AtomicInteger i = new AtomicInteger(0);
  public int getValue() { return i.get(); }
  private void evenIncrement() { i.addAndGet(2); } // 不能被中断的原子操作
  public void run() {
    while(true)
      evenIncrement();
  }
  public static void main(String[] args) {
    new Timer().schedule(new TimerTask() {
      public void run() {
        System.err.println("Aborting");
        System.exit(0);
      }
    }, 5000); // Terminate after 5 seconds
    ExecutorService exec = Executors.newCachedThreadPool();
    AtomicIntegerTest ait = new AtomicIntegerTest();
    exec.execute(ait);
    while(true) {
      int val = ait.getValue();
      if(val % 2 != 0) {
        System.out.println(val);
        System.exit(0);
      }
    }

}
} ///:~

在上面的例子中,我们使用AtomicInteger来代替synchronized关键字。不过需要强调的是,原子类被设计用来实现java.util.concurrent中的类,只有在少数情况下才需要在自己的代码中使用它们。一般而言直接使用锁(synchronize或者显式锁对象)更加安全。

** 6 总结:什么时候使用volatile?

volatile变量具备对某个对象的同步的读写的特性,尤其是内存同步特性。volatile不必要也不可能修饰final变量,因为final变量是不可改变的,而volatile的含义是易变的。当你对变量执行某些复杂操作,而又希望在执行这些操作的同时阻止其他线程对这些变量的访问时,你应该使用锁机制(synchronization或者是显式锁),而不是volatile。
    使用volatile的典型场景: 在一个线程中修改一个作为标志的变量,在另外一个线程中检查该标志变量; 最重要的是, 要写入的新值不依赖当前值。 上述原则使得诸如 x++ 或者是 x += 7之类的操作不适合使用volatile关键字,因为这类操作在java中并不是原子操作(在c c++中是原子操作),而是包含了读取当前值,增加,然后写入新值这样一系列的操作。一个普遍的误解是认为x++在java中是原子的。如果你需要这种原子性,则需要AtomicInteger或者相似的其他类。

    下面这个例子结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁” :
public class CheesyCounter {
    private volatile int value;
    public int getValue() { return value; }
    public synchronized int increment() {
        return value++;
    }
}

increment函数是同步的,所以不能并发执行,保证了多线程写操作的正确性。在写操作的同时,多个线程可以同时读value变量,在同一时间不同线程读取的值可以保证是一致的,并且可以跟写操作并发执行。

最后,留一个问题:是不是跟java的readLock% writeLock很像? java的读写锁是如何实现的呢?

参考文献:

http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html
http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
http://stackoverflow.com/questions/16719004/should-i-use-volatile-if-i-use-synchronized
 happens-before俗解 http://ifeve.com/easy-happens-before/
http://javamex.com/tutorials/synchronization_volatile_typical_use.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic_updaters.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic_updaters.shtml
http://javamex.com/tutorials/synchronization_volatile_dangers.shtml

时间: 2024-10-11 05:40:42

java并发之volatile关键字(TIJ21-21.3.3 21.3.4)的相关文章

Java并发之volatile关键字

引言说到多线程,我觉得我们最重要的是要理解一个临界区概念. 举个例子,一个班上1个女孩子(临界区),49个男孩子(线程),男孩子的目标就是这一个女孩子,就是会有竞争关系(线程安全问题).推广到实际场景,例如对一个数相加或者相减等等情形,因为操作对象就只有一个,在多线程环境下,就会产生线程安全问题.理解临界区概念,我们对多线程问题可以有一个好意识.Jav内存模型(JMM)谈到多线程就应该了解一下Java内存模型(JMM)的抽象示意图.下图: 线程A和线程B执行的是时候,会去读取共享变量(临界区),

Java并发之synchronized关键字深度解析(二)

前言 本文继续[Java并发之synchronized关键字深度解析(一)]一文而来,着重介绍synchronized几种锁的特性. 一.对象头结构及锁状态标识 synchronized关键字是如何实现的给对象加锁?首先我们要了解一下java中对象的组成.java中的对象由3部分组成,第一部分是对象头,第二部分是实例数据,第三部分是对齐填充. 对齐填充:jvm规定对象的起始内存地址必须是8字节的整数倍,如果不够的话就用占位符来填充,此部分占位符就是对齐填充: 实例数据:实例数据是对象存储的真正有

java点滴之volatile关键字特性

一 问题引入 JVM把内存分为两层,一层为大的主存,另外一个是工作内存(属于每个线程自己专属的),正常情况下,线程在用到某个变量的值时,都是先取到工作内存中进行处理,然后再写回主存,这样就会带来不同线程变量值不同步的问题. volatile字面意思是易挥发,不稳定,比如100个线程同时访问修改的一个字段值,那么这种值的特性明显和JVM中高速缓存机制不相符,这种值是不适合放在各线程自己的寄存器中的,那会导致100个线程中持有的副本都不一样. 二volatile修饰符作用 当一个变量被声明成 vol

理解java中的volatile关键字

Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量.这两种机制的提出都是为了 实现代码线程的安全性.Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分. volatile 写和读的内存语义: 线程 A 写一个 volatile 变量,实质上是线程 A

Java中的volatile关键字

一.计算机内存模型的相关概念 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,可能会涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢得多,因此如果任何时刻对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度,因此自CPU里面就有了高速缓存. 也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU高速缓存中,那么CPU进行计

java并发系列(六)-----Java并发:volatile关键字解析

在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题.一旦一个共享变量被 volatile关键字 修饰,那么就具备了两层语义:内存可见性和禁止进行指令重排序.在多线程环境下,volatile关键字 主要用于及时感知共享变量的修改,并使得其他线程可以立即得到变量的最新值,例如,用于 修饰状态标记量 和 D

Java transient和volatile关键字

关键字Volatile Volatile修饰的成员变量在每次被线程访问时,都强迫从主内存中重读该成员变量的值.而且,当成员变量发生变化时,强迫线程将变化值回写到主内存.这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值.Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比.这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化.而volatile关键字就是提示VM:

Java并发之synchronized关键字深度解析(三)

前言 本篇主要介绍一下synchronized的批量重偏向和批量撤销机制,属于深水区,大家提前备好氧气瓶. 说完synchronized锁的膨胀过程,下面我们再延伸一下synchronized锁的两种特殊处理,一种是锁的批量重偏向,一种是锁的批量撤销.JVM中有两个参数,BiasedLockingBulkRebiasThreshold和BiasedLockingBulkRevokeThreshold,前者默认阈值为20,控制批量重偏向:后者默认阈值为40,控制批量撤销.下面我们分别看一下它们是如

Java并发编程 Volatile关键字解析

volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 根据volatile的语义,我们可以看到,volatile主要针对的是并发三要素(原子性,可见性和有序性)中的后两者有实际优化作用. 可见性: 线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作.