作用

volatile的语义为:易变的、不稳定的;所以不能与 final 一起使用。

volatile只能修饰一个成员变量。

当volatile去声明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些线程或者程序修改,为了确保这个变量修改后,所有线程都能够‘‘看到’这个改动,虚拟机就必须采用一些特殊的手段,来保证这个变量的可见性等特点。

回到JMM中

Java内存模型(JMM)其实都是围绕着 原子性、有序性、可见性 展开的。

为了在适当的场合,确保线程间的原子性、有序性、可见性,java使用了一些特殊的操作或者关键字来声明、告诉虚拟机,在这个地方,要尤其注意,不能随便变动优化目标指令。这相当于屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

JMM实现原理

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。

PS:在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的,而在之后的版本实现中,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。

volatile保证原子性
/**
 * volatile保证原子性
 * 在32位下虚拟机运行此程序
 */
public class MultiThreadLong {

    public volatile static long t = 0;    //如果不用volatile修饰

    public static class ChangeT implements Runnable{

        private long to;
        public ChangeT(long to){
            this.to = to;
        }

        @Override
        public void run() {
            while(true){
                MultiThreadLong.t = to;
                Thread.yield();
            }
        }
    }

    public static class ReadT implements Runnable{
        @Override
        public void run() {
            while( true ){
                long tmp = MultiThreadLong.t;
                if( tmp != 111L && tmp != -999L && tmp != 333L && tmp != -444L ){
                    System.out.println( tmp );
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ChangeT( 111L  ) ).start(); 
        new Thread(new ChangeT( -999L ) ).start(); 
        new Thread(new ChangeT( 333L  ) ).start(); 
        new Thread(new ChangeT( -444L ) ).start(); 
    }


}

程序说明:

Java语言规范保证读或者写一个基本类型变量(除long和double外)是原子性的。

尝试在32位的虚拟机上运行上面的代码,查看加与不加 volatile 修饰 MultiThreadLong.t 变量的区别。

由于在32位的虚拟机中,long型的读与写的操作都不是原子性的,多线程之间发生了互相干扰,所以不加volatile修饰的话,可能的输出结果如下:

-4294966963
4294966852

当然volatile并不能替代锁,当需要保证一些程序中的符合操作的原子性时,需要使用synchronized或其他锁机制。

volatile保证数据的可见性和有序性
/**
 * volatile保证数据的可见性和有序性
 */
public class NoVisibility {

    private volatile static boolean ready ;    //尝试一下使用volatile修饰与不使用的区别
    private static int number = 0;
    public static class ReaderThread extends Thread{
        @Override
        public void run() {
            while( !ready );
            System.out.println( number );
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(100);
        number = 100;
        ready = true;
        Thread.sleep(500);
    }


}

程序说明:

如果ready变量不加volatile 修饰,那么,在ReaderThread线程start()之后,其他线程修改了ready = true ,ReaderThread线程也无法‘看到’其他线程的修改,程序则会一直不输出

实际上,由于JVM的优化工作,没有volatile 修饰的代码,会转变成这样的代码(这种优化叫做提升(hoisting))

if(!ready){
    while(true);
}

如果volatile修饰,则在主线程上修改了ready的值,ReaderThread线程将可以看到主线程中对ready变量的修改,程序输出: 100

volatile修饰的变量ready的作用类似于下面这段代码。

public class NoVisibility {

    private static boolean ready ;
    private static int number = 0;

    private static synchronized boolean isReady(){
        return ready;
    }
    private static synchronized void stopReady(){
        ready = true;
    }

    public static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!isReady());
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(100);
        number = 100;
        stopReady();
        Thread.sleep(500);
    }
}

results matching ""

    No results matching ""