
一、 一个悲伤的故事:
有家宠物店,按照店里规定,喂狗是先投食,狗吃完后, 在狗脖子挂一个牌子,表示狗已喂。
但是实际工作中,有时店员不按这个流程走,可能会先在狗脖子上挂一个牌子,然后再去取狗粮来喂。
某一天,店员在挂好牌子后,去厨房取狗粮,这时狗主人进来,发现狗挂了已喂的牌子,但是狗又饿的汪汪叫。
狗主人质疑店员没有喂,并向市场监督管理局投诉,市场监督局认真调查,发现狗确实没有被喂。
于是根据相关法律法规,关闭了这家店。。。
下面这段程序在线演绎上述故事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| 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。
上述方案,在单线程下不会有任何问题,但是在多线程环境中就不行了,模拟宠物店的程序输出结果也验证了这点。
原因参见下图:
我们以为的流程:

由于指令重排序,实际上可能会发生的流程:

三、 如何避免这种情况?
宠物店重新开张后,老板想了个解决办法。
首先不能禁止先挂牌子在喂食这种行为,因为禁止后会降低店员的工作效率,并且行为并没有损害客户的利益,只是容易让客户产生误解。
于是老板想了个好办法,给每只狗加个窗帘遮挡,具体流程如下:
1.准备给狗喂食,把窗帘拉上,让狗主人看不到狗的状态;
2.喂食(先喂食后上牌或者先上牌后喂食均可);
3.喂食结束,去除窗帘。此后狗主人才可以看到狗的状态。
java中有多重方法实现上述中窗帘的功能。出于演示程序的目的,本文使用ReentrantLock加锁方式解决前述问题。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| 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; } } }
|
多次运行后,程序结果均如下

至此,我们解决了困扰宠物店老板的问题。
加了锁后,先上牌后喂食的程序运行流程如下:

四、 总结
- java程序指令运行时存在指令重排序行为,虚拟机保证指令重排序不会影响单线程的执行结果,但不保证多线程下的执行结果符合预期;
- 多线程环境下,针对共享变量的访问,应当谨慎使用,适当加锁或同步机制,以避免出现数据不一致行为。