java线程间使用同一变量的结果

需求

一个仓库只能存放10个商品,库存数量始终要保持在10,如果卖出去几个,那么立马又从商品供应商那里买回来,保证仓库存满10个商品。

代码设计

做一个仓库线程类,此线程负责监听购买者来仓库取货的数量,用在线程里使用while(true)的方法实现监听功能。仓库线程类里有一个成员变量为stock,它的作用是用来记录商品库存数量。若监听到购买者购买取走商品,即stock<10,就能马上把stock重新设置为10,模拟从供应商中得到补给使得仓库始终保持10个商品的库存量的过程;但是购买者一次性进货数量超过10,即stock<0,则购买失败。

而此类中的main方法(主线程)可以充当购买者,通过线程对象调用buy方法来模仿买家购买取货,buy方法中的参数number表示一次性买多少。

代码实现

import java.util.Scanner;

public class Depository implements Runnable {

    //volatile指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
    private volatile int stock = 10;

    @Override
    public synchronized void run() {

        while (true) {
            /*
             *下面注释掉的try代码块,效果跟stock加volatile关键字修饰的输出结果一样,但是它的意义可以理解为”从商品卖出去到从供应商进到货的间歇时间“
             *通俗来讲,就是在仓库还没放满10个时,我先关仓100毫秒,给我100毫秒去取货,拿回来再开仓
             *try {  ②
             *    Thread.sleep(100);
             *} catch (InterruptedException e) {
             *    e.printStackTrace();
             *}
             *如果stock不加volatile修饰,子线程就一直读取cpu缓存中的stock,stock最初加载到stock缓存中的值为10,所以下面stock的值一直打印10
             *System.out.println(this.getStock());  ①
             * */
            if (this.getStock() < 0) {
                //失败回退
                System.out.println("仓库从供应商里进货" + (10 - this.getStock()) + "个商品失败!!!!!!!!原因:进货数量大于库存");
                System.out.println("-------------剩余" + this.getStock() + "个商品-------------");
            } else if (this.getStock() < 10) {
                //成功且通过内部供应商补给
                System.out.println("仓库从供应商里进货" + (10 - this.getStock()) + "个商品");
                System.out.println("-------------剩余" + this.getStock() + "个商品-------------");
            }
            //商品库存保持到10
            this.setStock(10);
        }
    }

    public Depository() {
    }

    public Depository(int stock) {
        this.stock = stock;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }

    public void buy(int number) {
        stock -= number;
        System.out.println("给购买者提供了" + number + "商品");
        System.out.println("-------------剩余" + stock + "个商品-------------");
    }

    public static void main(String[] args) {
        Depository depositoryRunnable = new Depository();
        Thread depositoryThread = new Thread(depositoryRunnable);
        depositoryThread.setDaemon(true);
        depositoryThread.start();
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //给子进程充足的时间进货,等子进程执行完再询问购买者是否要购买
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("\n请输入购买者来仓库进货的数量:");
            String input = scanner.next();
            if (!input.equals("exit"))
                depositoryRunnable.buy(Integer.parseInt(input));
            else
                break;
        }
    }
}

正常输出结果:
在这里插入图片描述

问题分析

以上都是最终的成果,但是我在编码和测试途中遇到很多问题,其中一个最大的问题就是有关“java线程循环监听类变量值的变化”的(终于扣回主题…)

当线程类的成员变量stock不用volatile修饰时,无论主线程中buy如何调用,子线程depositoryThread监听到的stock的值始终都是10,可以把代码中①部分取消注释执行一下,你就可以看到循环打印stock的值10,就是进不到if(stock<10)里面打印“从供应商里进货”。那么,为什么main方法主线程中调用buy方法把stock的值修改了,而子线程中监听不到stock的变化呢?③

学过java多线程的各位都应该知道synchronized是防止线程间抢占CPU资源,你以为volatile也是这样的作用吗?并不是,它的作用是使变量在线程间可见。那么我们就知道原因了,因为stock不用volatile修饰时,stock在线程间不可见(可以暂时理解成不共享),那么就会有很严重的线程安全问题,一个线程使用这个变量的过程中另一个线程读取或者写入变量,一个线程可能会读取到一个并不是我们期望的值。所以子线程depositoryThread监听不到主线程对stock的修改。

我们还是不用volatile修饰stock,但是,我们把上面代码的②部分的try块取消注释,再执行,发现代码恢复正常了,效果跟有volatile关键字修饰stock的一样。

在解释这个问题之前,得从底层说起,先说一下计算机CPU、内存和缓存的分工和区别:CPU是负责运算和处理的,内存是交换数据的。当程序或者操作者对CPU发出指令,这些指令和数据暂存在内存里,在CPU空闲时传送给CPU,CPU处理后把结果输出到输出设备上,输出设备就是显示器,打印机等。在没有显示完之前,这些数据也保存在内存里。CPU存取数据的速度非常的快,一秒钟能够存取、处理十亿条指令和数据。而内存就慢很多,快的内存能够达到几十兆就不错了。可见两者的速度差异是多么的大 。缓存就是为了解决CPU速度和内存速度的速度差异问题,缓存是CPU的一部分,它存在于CPU中。内存中被CPU访问最频繁的数据和指令被复制入CPU中的缓存,这样CPU就可以不经常到像“蜗牛”一样慢的内存中去取数据了,CPU只要到缓存中去取就行了,而从缓存获取数据的速度要比从内存中快很多。通常CPU找数据或指令的顺序是:先到缓存中找,找不到就到内存中找。

上面的程序从main方法入口开始执行,JVM把程序都加载到内存里了准备等CPU计算了。当程序执行到new一个Depository对象时,其stock变量也就被创建了,并且被赋予10的值。在stock被创建和赋值的过程中,CPU从缓存(好像有的也叫“工作内存”)中读取stock变量,发现找不到,找不到就去内存(好像有的也叫“共享内存”)中找,在内存中找到了!CPU直接把stock变量加载进来执行赋值等操作,并且把值为10的stock变量放入CPU缓存中,方便下次CPU中各线程的调用。继续往下执行到depositoryThread.start()时,子线程开启了,此时子线程也需要用到同一对象depositoryRunnable中的stock变量,用来判断是否小于0和小于10,CPU就优先从缓存中找stock变量,发现这个stock挺眼熟的嘛,不就是刚刚赋予10的值的那个家伙吗?于是子线程中一直while循环获取到的stock值就为10了。即使后面通过buy方法改变stock的值到内存中,子线程还是拿不到stock变化后的值。

回到问题③,是因为sleep方法使CPU缓存(工作内存)中的stock以及其他变量失效了,CPU只能在内存中访问stock的值了,而内存中stock的值,是buy方法修改后的stock值。
在这里插入图片描述
而volatile关键字,也可以使加载到CPU缓存的资源重新加载,使工作内存的变量重新生效。

volatile的原理

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该通过获取排他锁单独获取这个变量;
java提供了volatile关键字在某些情况下比锁更好用。

Java语言提供了volatile了关键字来提供一种稍弱的同步机制,他能保证操作的可见性和有序性。当把变量声明为volatile类型后,
编译器与运行时都会注意到这个变量是一个共享变量,并且这个变量的操作禁止与其他的变量的操作重排序。

访问volatile变量时不会执行加锁操作。因此也不会存在阻塞竞争的线程,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

volatile的内存语义

根据JMM要求,对于一个变量的独写存在8个原子操作。对于一个共享变量的独写过程如下图所示:
在这里插入图片描述
对于一个没有进行同步的共享变量,对其的使用过程分为read、load、use、assign以及不确定的store、write过程。
整个过程的语言描述如下:

  • 第一步:从共享内存中读取变量放入工作内存中(readload)
  • 第二步:当执行引擎需要使用这个共享变量时从本地内存中加载至CPU中(use)
  • 第三步:值被更改后使用(assign)写回工作内存。
  • 第四步:若之后执行引擎还需要这个值,那么就会直接从工作内存中读取这个值,不会再去共享内存读取,除非工作内存中的值出于某些原因丢失。
  • 第五步:在不确定的某个时间使用storewrite将工作内存中的值回写至共享内存。

由于没有使用锁操作,两个线程可能同时读取或者向共享内存中写入同一个变量。或者在一个线程使用这个变量的过程中另一个线程读取或者写入变量。
即上图中1和6两个操作可能会同时执行,或者在线程1使用num过程中6过程执行,那么就会有很严重的线程安全问题,
一个线程可能会读取到一个并不是我们期望的值。

那么如果希望一个线程的修改对后续线程的读立刻可见,那么只需要将修改后存储在本地内存中的值回写到共享内存
并且在另一个线程读的时候从共享内存重新读取而不是从本地内存中直接读取即可;事实上
当写一个volatile变量时,JVM会把该线程对应的本地内存中共享变量值刷新会共享内存;
而当读取一个volatile变量时,JVM会从主存中读取共享变量,这也就是volatile的写-读内存语义。

问题的根本原因,还是CPU的缓存机制,而sleep方法和volatile关键字都是防止线程只从CPU缓存中拿(另一个线程已修改的但是CPU缓存中还未更新的)过时资源。

参考url:https://www.cnblogs.com/heikedeblack/p/14779611.html
https://www.cnblogs.com/bmilk/p/13178009.html

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值