Java并发编程——浅谈volatile关键字

Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操做通知到其余线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,volatile变量不会被缓存在寄存器或者其余处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新的值。java

1.volatile关键字的两层语义c++

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:缓存

1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。优化

2)禁止进行指令重排序。atom

先看下面一段代码spa

//线程1
boolean alseep= false;
while(!alseep){
    doSomething();
}
 
//线程2
alseep= true;

 

这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。线程

那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。code

若是还不清楚明白,看线程读取变量的示意图?排序

好比:线程1修改了变量的值,也刷新到了主存中,可是线程二、线程3在此段时间内读取仍是工做内存中的,也就是说他们读取的不是“最新的值”,此时就出现了不一样的线程持有的公共资源不一样步的状况。内存

若是加入volatile修饰,就能够生效

//线程1
volatile boolean alseep= false;
while(!alseep){
    doSomething();
}
 
//线程2
alseep= true;

 

1:使用volatile关键字会强制将修改的值当即写入主存;

2:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

3:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取

若是仍是有点含糊,看下volatile读取变量的示意图?

若是在变量前加上volatile关键字,能够确保每一个线程对本地变量的访问和修改都是直接与主存进行交互。

而不是像上面与工做内存相交互。因此能够保证每一个线程均可以得到最新的值。

 

2.volatile保证原子性吗?

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         //保证前面的线程都执行完
        while(Thread.activeCount()>1)  
            Thread.yield();
        System.out.println(test.inc);
    }
}

这段程序的输出结果是多少?也许不少朋友认为是10000。可是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可是要注意,假如线程1对变量进行读取操做以后,被阻塞了的话,并无对inc值进行修改。而后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,可是线程1没有进行修改,因此线程2根本就不会看到修改的值。

根源就在这里,自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。

把上面的代码改为如下任何一种均可以达到效果:

采用synchronized:

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         //保证前面的线程都执行完
        while(Thread.activeCount()>1) 
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用原子性操做AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做类,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是原子性操做。volatile 修饰基本类型必需要肯定变量操做是原子操做,但count++并非原子操做,其中有读,加,写三个操做。

3.volatile能保证有序性吗?

volatile关键字能禁止指令重排序,因此volatile能在必定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1:当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;

2:在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

4.volatile的原理和实现机制

下面这段话摘自《深刻理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1:它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

2:它会强制将对缓存的修改操做当即写入主存;

3:若是是写操做,它会致使其余CPU中对应的缓存行无效。

使用volatile关键字的场景

不要将volatile用在getAndOperate场合(这种场合不原子,须要再加锁),仅仅set或者get的场景是适合volatile的。加锁机制既能够确保可见性又能够确保原子性,而volatile变量只可确保可见性。一般来讲,当且仅当知足如下全部条件时。才应该可使用volatile变量:

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

2:该变量不会与其余状态变量一块儿归入不变性的条件中。

3:在访问变量时不须要加锁。

相关文章
相关标签/搜索