Java并发编程volatile关键字理解

Java并发编程volatile关键字理解

一、volatile是什么?

volatile是jvm提供的轻量级的同步机制,它具有三大特性分别是:保证可见性、禁止指令重排序、不保证原子性。说到这三大特性还得提到JMM(java内存模型)。

1、JMM

每个线程创建时,jvm都会为其分配一个工作内存,这个工作内存是每个线程私有空间,然而我们定义的所有变量都是存在主内存中,主内存是共享的内存区域,创建的线程都可以访问,但是线程对于变量的操作,必须先将变量从主内存拷贝一份到自己的工作内存,然后才能对其进行操作,操作完毕再将变量写会主内存。各个线程之间不能直接访问对方的工作内存,必须通过主内存间接访问。以下是线程访问主内存的过程:
在这里插入图片描述

2、可见性

有两个线程A和B,分别从主内存拷贝了一个变量到自己的工作内存,当线程A对这个变量修改了之后再将此变量写回到主内存,而此时线程B并不知道主内存中的值发生了改变,但是线程B仍然对它之前拷贝的变量旧值操作,也要写回主内存,这时就会发生了写覆盖的线程安全问题,这时就需要一个可见性机制,volatile可以解决这个问题,原理为:当A线程对变量做操作了之后立刻刷新到主内存,并且强制缓存了该值的线程从主内存中重新读取最新的值,以下用代码演示:

public class VolatileTest {
    private static  int a=0;

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("线程"+Thread.currentThread().getName()+"开始执行");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=1;

            System.out.println("线程"+Thread.currentThread().getName()+"修改了值,a="+a);

        },"A").start();

        while (a==0){

        }
        System.out.println("线程"+Thread.currentThread().getName()+"结束");
    }
}

执行结果:
在这里插入图片描述
说明:线程A修改了a的值为1,但是主线程不可见,主线程的工作内存中a的值仍为0,所以一直在while循环中,不会执行下面的代码。

public class VolatileTest {
    private static volatile int a=0;

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("线程"+Thread.currentThread().getName()+"开始执行");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=1;

            System.out.println("线程"+Thread.currentThread().getName()+"修改了值,a="+a);

        },"A").start();

        while (a==0){

        }
        System.out.println("线程"+Thread.currentThread().getName()+"结束");
    }
}

运行结果:
在这里插入图片描述
说明:此时加上了volatile关键字,线程A修改了a的值,主线程立马可见,a的值为1不走循环体,执行后面的代码主线程结束。

3、禁止指令重排序

计算机在执行程序的时候,为了提高性能,编译器和处理器会对指令重排序,在多线程环境下可能会出现乱序执行的情况。以单例模式的DCL为例:

class SingleTonTest{
    private static SingleTonTest instance=null;

    private SingleTonTest(){}

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

在对象的创建过程中,其实经历了三个步骤:
1.分配对象内存空间;
2.初始化对象;
3.设置对象指向分配的内存地址,此时instance!=null。
在多线程环境下,由于指令重排,第2和第3可能会调换位置,当刚好执行完第3步时,instance!=null,cpu切换到另一个线程执行,发现instance!=null,于是直接返回,但是此时对象还未初始化,就会发生线程安全问题。
解决这个问题只需要在下面代码中加上volatile即可。

    private static volatile SingleTonTest instance=null;

4、不能保证原子性

原子性即一个操作不可分割,是一个完整的操作。
代码演示如下:

public class VolatileTest {
    private static volatile int a=0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    a++;
                }
            }).start();
        }
        //等待操作a的所有线程执行完毕后,再查看a的值
        TimeUnit.SECONDS.sleep(1);
        System.out.println("a的值为\t"+a);
    }
}

执行结果为:
在这里插入图片描述
说明:10个线程对a加1,1000次,预期结果应该是10000,但是结果可能小于10000,这是由于volatile不能保证原子性导致的,导致原子性问题是由于a++操作是分步执行的。
下面请看a++的具体实现:

public class Test {
    volatile int a=0;
    public void add(){
        a++;
    }
}

底层实现在汇编语言中是这样的

public class com.cmc.springboot.java.Test {
  volatile int a;

  public com.cmc.springboot.java.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field a:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field a:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field a:I
      10: return
}

我们看add方法,在2这个步骤getfield,是获取变量a的值值0,第6步iadd执行+1操作,第7步putfield,把累加后的值写回,由此可见a++是分3步执行的,如果在putfield执行之前,此线程被阻塞,cpu切换到另外一个线程完成+1,并写回到了主内存中a为1,再回到之前的线程继续执行putfield,把1再写回到主内存,最终结果变成了a的值为1,导致了原子性的问题。
原子问题的解决:

public class VolatileTest {
    private static volatile int a=0;
    private static AtomicInteger atomicInteger=new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    a++;
                    atomicInteger.getAndIncrement();
                }
            }).start();
        }
        //等待操作a的所有线程执行完毕后,再查看a的值
        TimeUnit.SECONDS.sleep(1);
        System.out.println("a的值为\t"+a);
        System.out.println("atomicInteger的值为:\t"+atomicInteger);
    }
}

执行结果:
在这里插入图片描述
说明:利用AtomicInteger来解决原子性问题,AtomicInteger是利用CAS算法来实现的,可保证原子性操作,CAS通道
以上是本人学习过程中对volatile的一些理解,如有不对之处请指出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值