volatile和锁

volatile变量自身具有下列特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

根据happends-before原则,volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。那么位于volatile总能读取到最新的些人的数据。但是volatile的复合操作,如valatile++,多线程中还是会出问题,所以不能使用volatile变量来做计数器,这样是不安全的,那么什么时候使用volatile呢?

volatile使用的使用不要依赖自身,计数器就是依赖自身。可以作为开关,当满足条件的时候做控制,当然这个条件不能依赖自身。

volatile的内存语义

从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义;volatile读与监视器的获取有相同的内存语义。

请看下面使用volatile变量的示例代码:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据volatile规则,2 happens before 3。
  3. 根据happens before 的传递性规则,1 happens before 4。

上述happens before 关系的图形化表现形式如下:

执行顺序我们都清楚了,那么执行结果呢?

这里A线程写一个volatile变量后,B线程读同一个volatile变量。a写入volatile变量后会将所有共享变量刷新到主存,当B读取volatile变量的时候,会读取主存中的共享变量,这样i的结果为1,而不是0。我们可以把volatie当做一个细粒度的锁来看,volatile控制了写-读的执行顺序,并且提供了内存可见性。

volatile写的内存语义如下:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

volatile读的内存语义如下:

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

其实这里有个问题,如果writer方法在执行的时候,有重排序的情况,执行顺序为flat=true;a=1;

如果是这样的话,我们还能确定结果吗?答案是否定的,因为放volitile写入的时候只能讲前面的结果刷新到主存,此时a=0,那么线程B读取到的a=0,这样结果不是我们想要的。

当然这种问题是不是出现的,因为volatile实现内存语义的时候已经做了控制,同样是使用了内存屏障来实现。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

这样可以达到什么效果呢?

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

这样可以发现之前担心的问题不会发生。

 

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而监视器锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,监视器锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。

下面看看锁,锁是java并发编程中最重要的同步机制

class MonitorExample {
    int a = 0;

    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}

假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens before规则,这个过程包含的happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
  2. 根据监视器锁规则,3 happens before 4。
  3. 根据happens before 的传递性,2 happens before 5。

上述happens before 关系的图形化表现形式如下:

 

锁的内存语义:

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

 

锁的内存语义和volatile的内存语义相似。对于锁的内存语义的实现会依赖volatile还有cas

posted on 2013-08-08 15:54  寻找真正的我  阅读(2076)  评论(0编辑  收藏  举报

导航