java中多线程之volatile详解(最通俗)

2021/7/29 22:09:36

本文主要是介绍java中多线程之volatile详解(最通俗),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

java中多线程之volatile详解


什么是volatile

volatile是JVM提供的轻量级同步机制

好,开始讲大家看不懂的东西了!
volatile有三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 有序性

傻了吧,这他妈都是些什么jb东西啊?别着急,我们一个一个来。

在学习volatile之前,我们先了解一下JMM。什么又是JMM?我只知道JVM。这他妈是啥东西啊?

JMM:java内存模型。jmm是一种抽象的概念,并不真实存在,它描述的是一种规范,通过这种规范定义了程序中的各个变量的访问形式。(仔细读,还是能读懂的)

JMM关于同步的规定(仔细读):
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁钱,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁

知道看不懂,开始白话文解释!

JVM我们的java虚拟机运行程序的时候,是以线程为最小刻度的。而每个线程创建的时候,jvm就会为这个线程创建一个工作内存,该工作内存是私有的,只能被当前线程所访问。
而JMM内存模型中规定:所有的变量都储存在主内存中,所有线程都能访问,但线程对变量的任何操作(读取赋值等)都必须在工作内存中进行,首先要将主内存中的变量拷贝到自己的工作内存中,然后才能对变量进行操作,操作完成后再讲变量写会主内存中。

这里我们发现了一个问题
先试想这样一个场景:现在有一个商品只剩下最后一个,如果两个线程同时进来抢,拿到了一个变量:int a = 1;(商品的数量) 这时候这个int a = 1;会拷贝出两份,分别存在于线程1的工作内存和线程2的工作内存。 我们知道,不同线程间是无法访问对方的工作内存的。

这个时候线程1 跑得快一点抢到了最后一个商品,把int a 的值-1了,然后通知快递部门上门来取货,把这最后一个商品拿走发货,然后把最新的a的值返回给主内存。现在主内存int a 的值等于0。

但对于线程2来说,它现在只看自己的工作内存,不看主内存,对于线程2来说,int a 的值现在还是1。所以它就觉得它也抢到了商品,其实这时主内存中的int a已经是0了,已经没有商品了。这时线程2把自己工作内存的int a 的值-1,然后通知快递部门来取货,快递来了发现你他妈的商品都卖完了我来取个啥?

上面就出现了超卖的情况,其根本原因就是:多个线程之间不能知道对方的对共享变量的执行情况,大家都是盯着自己的东西在做事。就像两个施工队在山的两边一起往中间打隧道,互相不知道对方的情况,最后两个隧道在山的中间完美错过。

好!那么有没有一个办法,只要有一个线程修改了主内存的变量的值以后,其他的线程能马上知道并获取到最新的值呢?

volatile的可见性

先看看没有使用volatile关键字的情况:

1.编写一个类,模拟售卖商品的过程,商品数量我们初始化为 Int a = 1;

class Shop{
        int a = 1;

        public void saleOne(){
            this.a = a-1;
        }
}

2.测试类

    public static void main(String[] args) {
        Shop shop = new Shop();

        new Thread(()->{
            System.out.println("线程A初始化");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            shop.saleOne();
            System.out.println("线程A购买商品完成,剩余商品量:"+shop.a);
        },"线程A").start();

        while (shop.a == 1){

        }

        System.out.println("主线程,剩余商品量:"+shop.a);
    }

这里有两个线程,线程A和主线程。 程序启动的时候:

  1. 线程A和主线程分别把shop 对象拷贝一份到自己的工作内存,对于这两个线程来说,shop 对象里的int a 属性的值都是1;
  2. 启动线程A
  3. 线程A等待3秒,再此3秒期间,主线程已经运行到while (shop.a == 1)这行代码,因为判断为true所以一直阻塞。
  4. 3秒过后,线程A调用方法,把自己工作内存中的int a 减 1,并把最新的int a = 0发送到主内存。
  5. 但我们发现,主线程还是一直处于阻塞的状态,对于主线程来说,它不知道int a 的值已经变为0,对主线程来说现在int a 的值还是自己工作内存中的1,所以 while (shop.a == 1)的判断永远为True。不会执行最后一行代码System.out.println(“主线程,剩余商品量:”+shop.a);

在这里插入图片描述

我们加上volatile关键字

class Shop{
    volatile int a = 1;

    public void saleOne(){
        this.a =a-1;
    }
}

测试代码不变
结果:
在这里插入图片描述

  1. 线程A和主线程分别把shop 对象拷贝一份到自己的工作内存,对于这两个线程来说,shop 对象里的int a 属性的值都是1;
  2. 启动线程A
  3. 线程A等待3秒,再此3秒期间,主线程已经运行到while (shop.a == 1)这行代码,因为判断为true所以一直阻塞。
  4. 3秒过后,线程A调用方法,把自己工作内存中的int a 减 1,并把最新的int a = 0发送到主内存。
  5. 由于volatile的可见性,此时对于主内存来说 int a的值已经由1变为了0, while (shop.a == 1)判断为False。程序就继续往下走,打印出了最新的int a的值:0。这时候商品数量为0之后,我们就不会再出现超卖的情况了

volatile的原子性(不保证)

原子性什么意思呢?
也就是完整性,比如一个线程在做一件事的时候,期间不能被加塞或分割,需要整体完整,要么同时成功要么同时失败。
大白话翻译:同一个方法,在一个线程没有执行完之前,其他线程必须给我等着。等我执行完了再放第二个线程进来。以免线程1的操作被线程2给覆盖了。比如synchronized,就保证了原子性。

给我们的Shop类创建一个增加商品库存的方法(每调一次addGoods方法,Int a就+1):

class Shop{

    volatile int a = 1;

    public void addGoods(){
        a++;
    }
    
    public void saleOne(){
        this.a =a-1;
    }


}

此时int a商品数量是加了volatile 修饰的,保证了不同线程之间的可见性!
测试:

    public static void main(String[] args) {
        Shop shop = new Shop();

        for(int i = 0; i < 20;i++){
            new Thread(()->{
                shop.addGoods();
            }).start();
        }
		
		//保证所有20个线程都跑完,只剩下2个线程(主线程和GC线程)的时候代码才继续往下走
		//其中 Thread.yield() 方法表示主线程不执行,让给其他线程执行
        while (Thread.activeCount() >2){
            Thread.yield();
        }

        System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.a);
    }
  1. 开20个线程去执行addGoods()方法
  2. 最后主线程把int a 的数值打印出来

结果让我们大失所望,每次执行程序得到的结果都不一样
在这里插入图片描述
在这里插入图片描述

这里我们知道,volatile不能保证程序的原子性。那为什么呢?
首先明确一点 a++操作不是原子性,它有三步:

  1. 获取主内存的当前值到自己的工作内存
  2. 进行+1操作
  3. 把最新值写回到主内存

尚且a++都不是原子操作,那我们平时的业务代码是不是更长,花的时间也更多?被其他线程覆盖的机会是不是也更大?

好,现在我们来看看上面的20个线程的例子怎么来分析!

  1. 加入现在有l两个线程几乎同时进入到addGoods()方法里面。
  2. 对于A,B两个线程而言,现在int a 的值都拷贝到各自的工作内存中,值都=1。
  3. 现在线程A开始执行a++操作,底层获取到当前值,然后+1,得到值为2,准备把最新值写入到主内存
  4. 这个时候由于多线程的机制,线程A在写入主内存之前被挂起了!
  5. 线程B开始执行了,成功的把int a 从加到2,写入主内存,现在主内存的值是2
  6. 线程A现在又被唤起,完成第3步没有完成的操作,把线程A自己工作内存中的2写入到主内存。
  7. 但现在主内存本来就是2,线程A由于在执行底层的++操作,没有机会去读取到最新的值。

以上!就是整个代码运行流程,解释了volatile为什么不能保证原子性。我知道很多同学还是没看懂,别急,文章最后会有更直观的例子(单例模式中的线程安全问题),一看就明白了

现在我们想一想,怎么解决volatile这个缺点呢?怎么实现原子性?

  • 1,在addGoods方法加同步锁synchronized
  • 2, AtomicInteger原子类

我们讲第二种:

修改我们的Shop类

class Shop{

    AtomicInteger atomicInteger = new AtomicInteger(1);

    public void addGoodsByAtomic(){
       atomicInteger.getAndIncrement();
    }
  1. 初始化原子类值为1
  2. 创建新方法,方法体让原子类自增1,整个过程是原子性的。

测试:

 public static void main(String[] args) {
        Shop shop = new Shop();

        for(int i = 0; i < 20;i++){
            new Thread(()->{
                shop.addGoodsByAtomic();
            }).start();
        }

        while (Thread.activeCount() >2){
            Thread.yield();
        }

        System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.atomicInteger);
    }

结果正确:
在这里插入图片描述
为什么原子类保证了原子性?这个设计到CAS锁。看我关于CAS的博客就懂了哈!

volatile的有序性(禁止指令重排)(了解)

这是什么鸡巴东西?
我们写的java代码,为了提高性能,在编译器和处理器中往往会进行指令重排,例如我写的某一行代码在23行,当经过编译过后这行代码在150行。

多线程环境中,由于编译器重排的原因,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

简单来说volatile避免了指令重排,也就避免了多线程中可能产生的问题。

volatile的运用场景(重点)

单例模式:

public class type3 {

    private static type3 type;

    private type3(){}


    private static type3 getInstance(){
        if(type == null){
            type = new type3();
        }
        return  type;
    }

}

上面是一个线程不安全的单例模式,我们可以加上一个synchronized :

public class type4 {

    private static type4 type;

    private type4(){}


    private static synchronized type4 getInstance(){
        if(type == null){
            type = new type4();
        }
        return  type;
    }

}

但synchronized把整个方法都锁了,在高并发的情况下,太重了。并发性下降了,吞吐量下降了。

所以出现了效率最高,也安全的单例模式写法:双重检查!

public class type5 {

    private static type5 type;

    private type5(){}


    private static type5 getInstance(){
        if(type == null){
            synchronized(type5.class){
                if(type == null){
                    type = new type5();
                }
            }
        }
        return  type;
    }

}

大家觉得上面的代码有没有什么问题?
我来梳理一下。

  1. 现在有A B两个线程同时进来,都通过了第一次检查。现在到达了synchronized同步锁外面
  2. A线程运气好,被先放进去了,再次检查发现type确实为null,好,放行
  3. A线程new了一个实例出来,这是把这个最新的实例返回给主内存,主内存的对象变量从Null变为有值
  4. A线程完成,B线程被放synchronized开始进行B线程的第二次检查
  5. 但由于type5 变量没有volatile修饰,所以线程B不能马上获取到最新的值,它不知道现在对象已经被new出来了,在线程B自己的工作内存了对象依然为null。
  6. B线程通过第二次检查,又new了一个对象出来。单例的目标没有达成,上面的代码失败。

所以我们要给变量加上volatile关键字:

private static volatile type5 type;
好了 基本已经讲完,欢迎大家评论区指出不足,一起学习进步!

大家看完了点个赞,码字不容易啊。。。



这篇关于java中多线程之volatile详解(最通俗)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程