通过示例验证java中的指令重排序问题

一、 一个悲伤的故事:

有家宠物店,按照店里规定,喂狗是先投食,狗吃完后, 在狗脖子挂一个牌子,表示狗已喂。
但是实际工作中,有时店员不按这个流程走,可能会先在狗脖子上挂一个牌子,然后再去取狗粮来喂。
某一天,店员在挂好牌子后,去厨房取狗粮,这时狗主人进来,发现狗挂了已喂的牌子,但是狗又饿的汪汪叫。
狗主人质疑店员没有喂,并向市场监督管理局投诉,市场监督局认真调查,发现狗确实没有被喂。
于是根据相关法律法规,关闭了这家店。。。

下面这段程序在线演绎上述故事:

package concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReOrderTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        PetShop petShop = new PetShop();

        for (int i = 0; i < 500000; i++) {
            DogHolder dogHolder = new DogHolder();
            service.execute(new Runnable() {
                @Override
                public void run() {
                    petShop.feedDog(dogHolder.dog());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    dogHolder.dog().check();
                }
            });
        }

        service.shutdown();
    }

    static class Dog {
        private static int count = 0;
        private final int id = ++count;
        // 是否饲养过
        private boolean hasFeed;
        private ReentrantLock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
        // 投食量
        private int feetNum;


        /**
         * 按照店里规定,喂狗是先投食,狗吃完后, 在狗脖子挂一个牌子,表示狗已喂。
         *但是实际工作中,有时店员不按这个流程走,可能会先在狗脖子上挂一个牌子,然后再去取狗粮来喂。
         * 某一天,店员在挂好牌子后,去厨房取狗粮,这时狗主人进来,发现狗挂了已喂的牌子,但是狗又饿的汪汪叫。
         * 狗主人质疑店员没有喂,并向市场监督管理局投诉,市场监督局认真调查,发现狗确实没有被喂。
         * 于是根据相关法律法规,关闭了这家店。。。
         */
        public void feed() {
            feetNum = 1;
            hasFeed = true;
        }

        public void check() {
            if (hasFeed) {
                if (feetNum == 0) {
                    System.out.println(this + "的主人震惊了!宠物店说喂了狗但是投食量却是0,这是一家黑心店!!");
                    System.out.println(this + "的主人向市场监督局投诉了这家店!!");
                }
            }
        }

        @Override
        public String toString() {
            return "Dog#" + id;
        }
    }

    // 宠物店
    static class PetShop{
        public PetShop() {
            System.out.println("宠物店开门营业...");
        }

        public void feedDog(Dog dog) {
            dog.feed();
        }
    }

    // 狗主人
    static class DogHolder{
        private Dog dog;

        public DogHolder() {
            this.dog = new Dog();
        }

        public Dog dog(){
            return dog;
        }
    }
}

多次运行程序,出现文章开头的一幕::
在这里插入图片描述

为什么发生上述结果呢?明明feed方法是先喂狗,再给狗挂个牌子,看起来没有问题。

二、 feed方法发生了什么?

输出表示在程序的多次运行中,出现过hasFeed为true,而feetNum仍然等于0的情况,这就验证了java内部指令重排序的情况。
再看下面这个例子:

int a = 1;
int b = 2;
int c = a + b

以A表示a的赋值,B表示b的赋值,C表示相加。
在java程序实际执行中,处于性能优化的目的,A和B的执行顺序可能会颠倒,比如先执行B,再执行A。
但是不管怎么优化,C的执行不会到A或B的前面,也就是说java程序会保证A和B均执行结束,才会执行C。
上述方案,在单线程下不会有任何问题,但是在多线程环境中就不行了,模拟宠物店的程序输出结果也验证了这点。
原因参见下图:
** 我们以为的流程: **
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyWx8bjt-1616827546097)(/images/Snipaste_2021-02-28_20-56-09.png)]

** 由于指令重排序,实际上可能会发生的流程: **
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVg95VZu-1616827546101)(/images/Snipaste_2021-02-28_21-02-52.png)]

三、 如何避免这种情况?

宠物店重新开张后,老板想了个解决办法。
首先不能禁止先挂牌子在喂食这种行为,因为禁止后会降低店员的工作效率,并且行为并没有损害客户的利益,只是容易让客户产生误解。
于是老板想了个好办法,给每只狗加个窗帘遮挡,具体流程如下:

1.准备给狗喂食,把窗帘拉上,让狗主人看不到狗的状态;
2.喂食(先喂食后上牌或者先上牌后喂食均可);
3.喂食结束,去除窗帘。此后狗主人才可以看到狗的状态。

java中有多重方法实现上述中窗帘的功能。出于演示程序的目的,本文使用ReentrantLock加锁方式解决前述问题。代码如下:

package concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class ReOrderTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        PetShop petShop = new PetShop();

        for (int i = 0; i < 500000; i++) {
            DogHolder dogHolder = new DogHolder();
            service.execute(new Runnable() {
                @Override
                public void run() {
                    petShop.feedDog(dogHolder.dog());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    dogHolder.dog().check();
                }
            });
        }

        service.shutdown();
    }

    static class Dog {
        private static int count = 0;
        private final int id = ++count;
        // 是否饲养过
        private boolean hasFeed;
        private ReentrantLock lock = new ReentrantLock();
        // 投食量
        private int feetNum;

        public void feed() {
            try {
                lock.lock();
                feetNum = 1;
                hasFeed = true;
            } finally {
                lock.unlock();
            }
        }

        public void check() {
            try {
                lock.lock();
                if (hasFeed) {
                    if (feetNum == 0) {
                        System.out.println(this + "的主人震惊了!宠物店说喂了狗但是投食量却是0,这是一家黑心店!!");
                        System.out.println(this + "的主人向市场监督局投诉了这家店!!");
                    }
                }
            } finally {
                lock.unlock();
            }
        }

        @Override
        public String toString() {
            return "Dog#" + id;
        }
    }

    // 宠物店
    static class PetShop{
        public PetShop() {
            System.out.println("宠物店开门营业...");
        }

        public void feedDog(Dog dog) {
            dog.feed();
        }
    }

    // 狗主人
    static class DogHolder{
        private Dog dog;

        public DogHolder() {
            this.dog = new Dog();
        }

        public Dog dog(){
            return dog;
        }
    }
}

多次运行后,程序结果均如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXT80Orq-1616827546104)(/images/Snipaste_2021-02-28_21-30-29.png)]

至此,我们解决了困扰宠物店老板的问题。
加了锁后,先上牌后喂食的程序运行流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPSVCFOk-1616827546106)(/images/Snipaste_2021-02-28_21-34-57.png)]

四、 总结

  • java程序指令运行时存在指令重排序行为,虚拟机保证指令重排序不会影响单线程的执行结果,但不保证多线程下的执行结果符合预期;
  • 多线程环境下,针对共享变量的访问,应当谨慎使用,适当加锁或同步机制,以避免出现数据不一致行为。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值