Java中volatile除了保证可见性还有什么用

最早接触到 volatile 的关键字的时候, 是用在多线程控制地方,一个主的线程通过 quitFlag 标志来控制子线程的启停,子线程通过循环来判断标记是否为 true,为 true 则退出,这时候如果不用 volatile 关键字修饰 quitFlag 在主线程更改后, 子线程可能无法立刻看到修改,导致无法及时退出的问题,甚至无法退出的问题。

一 volatile 保障了可见性

上面情况,如果用 volatile 来修饰 quitFlag 关键字,则可以及时退出。

public class TestQuitFlag {

 // 这种可能无法即时退出
 // private static  boolean quitFlag = false;
// 这种情况可以正常退出
private static  volatile boolean quitFlag = false;

    public static void main(String [] args) throws InterruptedException {
        new Thread(){
            @Override
            public void run() {
               while (!quitFlag) {
                   System.out.println(Thread.currentThread()+" is running");
                   try {
                       Thread.sleep(2000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               System.out.println("Child thread is stop");
            }
        }.start();
        Thread.sleep(3000);
        quitFlag = true;
        System.out.println("Main is exit..");
    }
}

原因是 Java 对缓存进行了抽象,java 的 JMM 内存模型,将线程访问的内存分为工作内存和主内存,工作内存只有本线程才可以操作,Java 操作的数据先保存到本地内存中,更改后刷新到主内存中,其他线程读取变量的时候每次都从主内存中同步到它的本地内存中,如下图:

二 volatile 与线程安全

volatile 保障了可见性,不具有原子性,不能保障线程的安全。有些说法可以部分保障线程安全,我认为那种可见性不能算是线程安全。简单的测试下,累加这种典型的场景:

import java.util.ArrayList;
import java.util.List;

public class TestCounter {

    private static volatile int  count = 0;

    public static void main(String [] args) throws InterruptedException {
        List<Thread>  threads = new ArrayList<>();

        for (int i = 0; i< 10 ; i++) {
            threads.add(new Thread(){
                @Override
                public void run() {
                   for (int j = 0; j < 1000; j++) {
                       count++;
                   }
                }
            });
        }
        threads.forEach(thread->{thread.start();});
        threads.forEach(thread->{
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("Main is exit..");
        System.out.println("result:" + count);
    }
}

一共启动 10 个线程,每个线程计数 1000 次,如果是线程安全的结果应该是 10000,打印结果如下:

如果改动下,通过 synchronized 来控制累加,代码如下:

import java.util.ArrayList;
import java.util.List;

public class TestQuitFlag {
    private static volatile int  count =0 ;
    public static void main(String [] args) throws InterruptedException {
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i< 10 ; i++) {
            threads.add(new Thread(){
                @Override
                public void run() {
                   for (int j = 0; j < 1000; j++) {
                       synchronized (TestQuitFlag.class) {
                           count++;
                       }
                   }
                }
            });
        }
        threads.forEach(thread->{thread.start();});
        threads.forEach(thread->{
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("Main is exit..");
        System.out.println("result:" + count);
    }
}

通过 synchronized 包下代码块,执行的结果就是 10000 了,这里面要注意下所有线程的 synchronized 的传入参数要是同一个对象,如果不是,则达不到锁的目的。比如刚才代码中:

synchronized(TestQuitFlag.class) 改成synchronized(this)是操作不同的对象,则达不到锁的目的。

当然在 java 中有性能更高的累加方法,那就是采用 Atomic*系列类,这些类因为采用 CAS 的方式进行加锁,所以性能更好些,这里就不再举例了。

三 volatile 可以防止指令重排

volatile 可以防止指令重排,JVM 虚拟机在执行 Java 字节码的时候,为了提升性能,在不影响程序语义的情况下,会对指令进行重排。当然除了 JVM,编译器或 cpu 都可能会进行指令重排。

典型的代码场景是双重锁检查单例写法,具体展示如下:


public class Singleton {
    private static volatile  Singleton singleton;

    private Singleton() {

    }
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null ) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

这里面必须给 singleton 添加 volatile 关键字,为什么要添加关键字,这里和 volatile 的防止指令重排问题有关。这里面主要和singleton = new Singleton();这句代码相关, 这句代码实际执行的时候分为三步操作:

  1. 申请一块内存。

  2. 调用 Singleton 的构造函数初始化。

  3. 将 singleton 引用指向这块内存空间,这样 singleton 执行就不是 null 了。这三步处于锁控制的范围内,当时如果没有 volatile 情况下,会发生指令重排,而引起错误。来举个例子:1) 线程 1 执行到 synchronized 同步代码块中,判断 singleton 为 null,这时候开始执行singleton = new Singleton();代码。2) 由于指令产生了重排,所以执行的代码顺序是 1->3->2 , 执行完 3,之后 singleton 不是 null 了,这时候线程时间片时间到,线程休眠。3) 其他线程再调用getInstance()判断 singleton 不为 null,直接返回 singleton 使用,当时我们知道,其实这个变量现在是未初始化的。其他线程使用了这个未初始化的变量,从而造成问题。

volatile 关键字给 JVM 指明修饰的字段可能在其他的线程中发生修改,所以

如下图:加上 volatile 关键字后,看 JVM 编译后的代码会多一句:

lock addr $0x0,(%esp)

这个指令相当于一个内存屏障,只有一个 cpu,并不需要;如果有两个或两个以上 cpu 访问访问的时候,会将 cache 本地内存的数据同步到主内存中,通过这个操作让 volatile 变量在其他的内存中立刻可见,也保证了后续的指令不能重排到 lock 指令之前。

顺便说下,DCL 实现的单例模式,还常被问到的点,为什么两次判断 singleton 是否为 null。顺便说下:

  1. 第一次判断 singleton 是否为 null,在不为 null 的时候可以不用进入到同步代码块,快速返回,提升了性能。

  2. 第二次判断 singleton 是否为 null,一个线程在判断 singleton 为 null,进入到同步代码块之前休眠了,这时候另外一个线程因为判断 singleton 为 null,则先进入了同步代码块,执行完毕后;开始的线程仍然可以进入同步代码块,如果不判断 singleton 是否为 null,则会再次创建个单例对象,违反了我们的单例的初衷。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值