Volatile底层原理级别详解---Java多线程


1.Volatile简介

Volatile可以说是JVM提供的最轻量级的同步机制了,但是它并不容易被完全正确的去理解,而且它不能绝对的保证线程的同步,也就是说在一定条件下,volatile可以保证线程的同步.这也就导致了很多程序员会下意识的避免使用它.从而选择更加稳妥的synchronized关键字.但是Volatile在很多地方都会使用到,对理解多线程的其他很多操作也是有着巨大意义的.

2.Volatile的作用一—可见性

如果大家了解**JMM(java内存模型)**的话就会知道:如果没有特殊情况的话,一个线程对于主内存中的共享变量的读写会在线程自己的工作内存中(大家可以简单理解为工作内存就相当于缓存).所以当一个线程在对一个共享变量进行修改之后,其他的线程并不能立马了解到.而使用了volatile关键字对变量进行修饰之后,某个线程对于Volatile变量的修改对其他线程就可以立马得知.只就是Volatile的可见性.下面通过一个对比来展示.

public class Volatile
{
    
    
    static boolean flag = true;//定义了一个全局变量
    public static void main(String[] args) throws InterruptedException
    {
    
    
        Thread thread1=new Thread(new Runnable() {
    
    
            @Override
            public void run()
            {
    
    
               while (flag)//当flag为true的时候会一直执行
               {
    
    

               }
            }
        });
        thread1.start();//开启第一个线程

        Thread.sleep(1000);//保证第一个线程一定是比第二个线程先执行
       new Thread(new Runnable() {
    
    
            @Override
            public void run()
            {
    
    
                System.out.println("stop thread1");
                flag=false;//将全局变量设置为false
            }
        }).start();
    }
}

将这段代码运行后会发现,即使第二个线程已经执行了flag=false;的操作,但是第一个线程还是会继续执行.因为线程二中的工作区中的flag还是为true,它对其他线程进行flag的修改并不知道.

可是如果我们将flag声明为Volatile之后,就不会出现这样的问题了.

3.Volatile的作用二—禁止指令重排序

指令重排序是指在单个线程内,在不影响最终结果的情况下,JVM会对代码进行乱序执行,以充分的利用系统的资源.比如

public class ThreadTest
{
    
    
    static int b,c;
    static int  a;
    private static class ReaderThread extends Thread
    {
    
    
        @Override
        public void run()
        {
    
    
            a=1;  b=2; c=a+b;
        }

    }
}

在这段代码中,就有可能产生指令重排序,a=1和b=2的操作顺序不一定会按照代码的顺序执行,因为这两个操作就算顺序颠倒,在一个线程内它是不会影响最终的结果,但是c=a+b.这个操作一定是在a=1和b=2之后执行.因为这样才能保证最终结果是对的.

在一个线程中重排序不会影响最终的结果.但是如果有另一个线程依赖于该线程中的中间结果,那么就有可能产生错误.而Volatile则可以通过内存屏障来禁止指令重排序.

public class ThreadTest
{
    
    
    static int b,c;
    volatile static int  a;
    private static class ReaderThread extends Thread
    {
    
    
        @Override
        public void run()
        {
    
    
            a=1;//内存屏障,保证在这之后的代码不会重排序到内存屏障之前
            b=2; c=a+b;
        }
    }
}

4.Volatile原理分析

如果查看对一个volatile变量进行读写的操作的编译后的代码会发现,与普通变量相比多了一个操作:lock addl $0x0,(%esp).这个操作的作用其实就是相当于一个内存屏障(Memory Barrier). 其中的lock前缀是关键.后面的addl $0x0,(%esp)是一个空操作,不会产生任何影响.

就是lock操作保证了volatile的可见性和禁止重排序.

lcok操作的作用就是将本线程的工作内存的值写入到了主内存中,这样的操作会有两个作用.

  1. 会使得其他线程的工作内存无效化,也就是其他线程对于volatile变量的读和写操作都必须直接从主内存中获取.从而保证了可见性
  2. 由于会将工作内存中的缓存写入到主内存中相当于告诉JVM:我这些操作已经完成了,后面的操作不能在我之前完成,从而产生内存屏障.

5.Volatile不能保证原子性.

原子性简单来说就是一个操作是不可分割的.但是volatile不能保证对于变量的操作是原子性的.举一个例子:假设有一个volatile的变量a,我们对进行a++操作.表面上a++就是一个操作但是其中是有三步:

  1. 读取a的初始值(volatile保证读取的一定是最新的值)
  2. 计算a+1
  3. 将a+1的值赋值给a(volatile保证赋值后的a会写入到主内存中)

大家想一想假如在进行第二步计算a+1的时候,别的线程已经将a的值改变了,那么最终的结果就是错误的.

6.Volatile的使用场景

由于volatile不能保证原子性,所以将其运用到同步时会有一定的限制,在不符合一下两条规则的运算场景下,我们仍需要通过其他的手段来保证原子性

  1. 运算结果并不依赖变量的当前值或者能保证只有单一线程能对变量进行修改
  2. 变量不需要与其他的状态变量共同参与不变约束.

同步时会有一定的限制,在不符合一下两条规则的运算场景下,我们仍需要通过其他的手段来保证原子性

  1. 运算结果并不依赖变量的当前值或者能保证只有单一线程能对变量进行修改
  2. 变量不需要与其他的状态变量共同参与不变约束.

猜你喜欢

转载自blog.csdn.net/qq_44823898/article/details/110734723