JUC(九):虚假唤醒

原文链接:http://www.jianshu.com/p/94d13f01c33c

原创文章,转载请注明原文章地址,谢谢!

生产者消费者案例

我们先用经典的生产者消费者案例来引出问题。

//产品
public class Clerk {

    private int product = 0;

    //生产产品
    public synchronized void get() {
        if (product >= 10) {
            System.out.println("产品已满!");
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
        }
    }

    //销售产品
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("产品售空!");
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
        }
    }
}
//生产者
public class Producer implements Runnable {

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.get();
        }
    }
}
//消费者
public class Consumer implements Runnable {

    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}
//测试
public class ProducerAndConsumerForLockTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        new Thread(producer, "生产者 A").start();
        new Thread(consumer, "消费者 B").start();
    }
}

测试结果

生产者 A : 1
生产者 A : 2
生产者 A : 3
生产者 A : 4
生产者 A : 5
生产者 A : 6
生产者 A : 7
生产者 A : 8
生产者 A : 9
生产者 A : 10
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
消费者 B : 9
消费者 B : 8
消费者 B : 7
消费者 B : 6
消费者 B : 5
消费者 B : 4
消费者 B : 3
消费者 B : 2
消费者 B : 1
消费者 B : 0
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!
产品售空!

通过上述案例,我们发现,在生产产品达到上限的时候,还在不断的生产,而在销售产品已经售空的情况下,依然在不停的销售产品,那么此时就出现了问题。那如果解决这样的问题呢?这就要使用到下面的等待唤醒机制。

等待唤醒机制

通过上面的案例,我们在这里引入等待唤醒机制。所谓等待唤醒机制,就是当一个线程在执行了某一个操作的时候,将其进入等待状态,并释放锁,其他线程执行完指定的操作后,再将其唤醒。

public class Clerk {

    private int product = 0;

    //生产产品
    public synchronized void get() {
        if (product >= 1) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
            this.notifyAll();
        }
    }

    //销售产品
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("产品售空!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
            this.notifyAll();
        }
    }
}

测试结果

产品售空!
生产者 A : 1
产品已满!
消费者 B : 0
产品售空!
生产者 A : 1
产品已满!
消费者 B : 0
产品售空!
生产者 A : 1
产品已满!
消费者 B : 0
......

此时从结果中可以看出,还算是比较和谐的一个结果。但是我发现一个问题,就是我在运行多次的时候,出现了不一样的结果,其中有一种情况就是,程序一直没有停止,我们来演示一下这个现象。

//生产者
public class Producer implements Runnable {

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.get();
        }
    }
}

程序运行结果发现,程序确实一直没有停止,那这是什么原因呢?这里的改动只不过是将生产者中的run方法,阻塞了200ms。

我们假设现在产品为0,消费者剩下最后一次循环,生产者剩下2次循环。

  • 消费者抢到锁,执行sale方法,此时产品售空,然后执行wait方法,消费者线程被挂起,并释放锁。消
  • 生产者循环次数为2,消费者剩余循环次数为0,产品数量为0。
  • 生产者拿到锁,执行get方法,生产产品,并执行notifyAll方法,唤醒消费者线程。
  • 生产者循环次数为1,消费者剩余循环次数为0,产品数量为1。
  • 此时消费者线程被唤醒,这里要注意,被唤醒之后,它是从上次被挂起的地方,继续往下执行,而在本程序中,往下执行将不会进行其他操作,消费者线程结束。
  • 生产者拿到锁,执行get方法,此时产品已满,然后执行wait方法,生产者线程被挂起,并释放锁。但是此时消费者线程已经结束,将再也没有线程来唤醒生产者线程,所以程序一直处理运行状态。

其实解决这个问题也很容易,我们将程序中的else语句去掉即可。

虚假唤醒问题

上面的案例,我们使用等待唤醒机制,似乎已经解决了问题,但是我们在上述案例中的测试中,只是使用了两个线程,那如果我们使用多个线程,会不会出现问题呢?

public class ProducerAndConsumerForLockTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        new Thread(producer, "生产者 A").start();
        new Thread(consumer, "消费者 B").start();
        new Thread(producer, "生产者 C").start();
        new Thread(consumer, "消费者 D").start();
    }
}

测试结果

产品售空!
产品售空!
生产者 A : 1
产品已满!
消费者 D : 0
产品售空!
消费者 B : -1
产品售空!
消费者 D : -2
产品售空!
......

我们已经发现,已经出现了负数,显然已经出现了问题。我们来简单分析一下原因。其实很好理解,当两个消费者线程同时执行sale方法时,产品售空,那么都将执行wait方法,处于挂起等待状态,并释放锁,然后生产者拿到锁,生产产品,执行notifyAll方法,唤醒了所有消费者线程,那么当第一个消费者执行了消费以后,第二个消费者又进行消费,此时便出现了负数,出现了问题。像这样的情况,就叫做虚假唤醒。
那么如果解决这个问题呢?我们只需要将判断的if换成while即可。换句话说,为了避免虚假唤醒问题,应该将判断一直使用在循环中。

//产品
public class Clerk {

    private int product = 0;

    //生产产品
    public synchronized void get() {
        while (product >= 1) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();
    }

    //销售产品
    public synchronized void sale() {
        while (product <= 0) {
            System.out.println("产品售空!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }
}

测试结果

生产者 A : 1
产品已满!
产品已满!
消费者 D : 0
产品售空!
产品售空!
生产者 C : 1
产品已满!
产品已满!
消费者 B : 0
产品售空!
产品售空!
......

博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!

转载于:https://www.jianshu.com/p/94d13f01c33c

展开阅读全文
博主设置当前文章不允许评论。

没有更多推荐了,返回首页