ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2021-07-29 20:03:35  阅读:170  来源: 互联网

标签:shop java int volatile 内存 线程 多线程 type


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;
好了 基本已经讲完,欢迎大家评论区指出不足,一起学习进步!

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

标签:shop,java,int,volatile,内存,线程,多线程,type
来源: https://blog.csdn.net/J169YBZ/article/details/119151121

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有