一个阻塞队列引发的死锁和伪唤醒

在学习JUC框架的时候,想自己模拟一个阻塞队列,先参考了一个例子:
package test;

import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;

public class DeadLockBlockingQueue {

    private Object notEmpty = new Object();
    private Object notFull = new Object();
    private Queue linkedList = new LinkedList();
    private int maxLength = 10;

    public Object take() throws InterruptedException {
        synchronized (notEmpty) {
            if (linkedList.size() == 0) {
                System.out.println("empty take");
                notEmpty.wait();
            }
            synchronized (notFull) {
                if (linkedList.size() == maxLength) {
                    notFull.notifyAll();
                }
                return linkedList.poll();
            }
        }
    }

    public void offer(Object object) throws InterruptedException {
        synchronized (notEmpty) {
            if (linkedList.size() == 0) {
                notEmpty.notifyAll();
            }
            synchronized (notFull) {
                if (linkedList.size() == maxLength) {
                    System.out.println("full offer");
                    notFull.wait();//这里会将生产者线程阻塞,此时生产者线程会将notFull释放,但是不会释放notEmpty,所以造成死锁
                }
                linkedList.add(object);
            }
        }
    }

    public static void main(String[] args) {
        final DeadLockBlockingQueue a = new DeadLockBlockingQueue();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        a.offer(UUID.randomUUID().toString());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "生产者").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        a.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "消费者").start();
    }
}
        wait方法属于Object,在一个对象上调用wait,会让当前线程释放这个对象的监视器,并进入休眠。但是这个线程所持有的其他对象的同步资源,并不会被释放。

        上面代码在notFull上调用了wait,会导致生产者休眠,并释放notFull,但是不会释放notEmpty。那么消费者拿不到notEmpty对象的监视器,这就造成了死锁。
        后来经朋友提醒,做了一些修改,将其改成了只有一个锁的方式:
package test;

import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;

public class BlockingSizeQueue {

    private int maxSize = 16;
    private Object lock = new Object();
    private Queue queue = new LinkedList();
    
    public Object take() throws InterruptedException {
        synchronized (lock) {
            if (queue.size() == 0) lock.wait();
            if (queue.size() == maxSize) lock.notify();
            return queue.remove();//TODO 这里为什么会报错?为什么size为0的时候,消费者没有被阻塞?
        }
    }
    
    public void offer(Object t) throws InterruptedException {
        synchronized (lock) {
            if (queue.size() == maxSize) lock.wait();
            if (queue.size() == 0) lock.notify();
            queue.add(t);
        }
    }
    
    public static void main(String[] args) {
        BlockingSizeQueue blockingSizeQueue = new BlockingSizeQueue();

        new Thread(new Producer(blockingSizeQueue), "生产者").start();
        new Thread(new Customer(blockingSizeQueue), "消费者").start();
    }
}

class Producer implements Runnable {
    private BlockingSizeQueue queue;
    public Producer(BlockingSizeQueue queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                queue.offer(UUID.randomUUID().toString());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Customer  implements Runnable {
    private BlockingSizeQueue queue;
    public Customer(BlockingSizeQueue queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}        LinkedList的remove方法会在集合为空时抛出NoSuchElementException。

        依据上面代码的逻辑,在集合为空时,消费者线程就会wait。当生产者线程向集合中添加了元素,并把消费者线程唤醒后,消费者线程就会和生产者线程进行lock的竞争了,谁争到了谁执行。
        那这里不应该出现NoSuchElementException啊,为什么呢?肯定是wait出问题了,因为消费者并没有因为wait的调用而休眠!
        那就看看wait吧,wait很简单,调用了自己的重载方法wait(long timeout),而这个方法是本地方法,那我们只能看他的注释了:
让当前线程处于等待状态,直到另外一个线程在这个object上调用notify方法或者notifyAll方法。或者其他线程中断了当前线程,或者指定的时间到了。
当前线程必须拥有这个object的监视器。
该方法会让当前线程(这里以T代称)把自己放到这个对象的等待集合中,并放弃对这个对象的所有同步要求。线程T将会停止执行进入休眠状态,直到下面4中情况之一发生为止:
1.其他线程在这个对象上调用了notify方法,而且线程T被选中为要唤醒的线程
2.其他线程在这个对象上调用了notifyAll方法
3.其他线程调用了这个线程的interrupt方法
4.指定的时间到了。另外,如果参数timeout是0,相当于没有指定时间,线程的唤醒就和时间无关了
线程T被唤醒时,会从object的等待集合中退出,重新进入可执行队列中。它会和之前一样去和其他线程竞争获取object的同步权利;一旦线程获取了object的控制权,它在这个object上的同步权利就会恢复到wait调用之前的状态。线程会接着从调用wait的地方继续执行。因此,从wait方法返回之后,线程T和object的同步状态都正确的回到了wait方法调用时的状态。
一个线程也可以在上述4中情况之外醒来,这被称为伪唤醒(spurious wakeup).虽然这在实践中很少发生,程序还是必须要防止这种情况,通过条件测试来验证被唤醒的线程是否应该继续wait还是running。换句话说,waits应该总是出现在循环中,像下面这样:
    synchronized(obj) {
        while (适合1处执行的条件不成立) {
            obj.wait(timeout);
        }
        
        //1
    }
关于这个话题的更多信息,可以看Doug Lea的《Concurrent Programming in Java(Second Edition)》中的3.2.3节,或者Joshua Bloch的《Effective Java Programming Language Guide》的第50条。
如果当前线程在waiting或者wait之前被调用了interrupt方法,使其状态处于interrupted的话,一个InterruptedException就会被抛出。抛出的时机是线程对object的锁定状态恢复到上面描述的时候。


记住,wait方法只会让当前线程进入这个object的等待集合中,并解除当前线程对这个object的锁定。当前线程持有的其他对象的锁在线程等待的时候,会被保留。
wait方法只应该被拥有object监视器的线程调用。请参考notify方法对线程如何成为一个监视器的拥有者的描述。
        好了,罪魁祸首原来就是伪唤醒啊!那我们把上面的if换成while再试试:
package test;

import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;

public class BlockingSizeQueue1 {

    private int maxSize = 16;
    private Object lock = new Object();
    private Queue queue = new LinkedList();
    
    public Object take() throws InterruptedException {
        synchronized (lock) {
            while (queue.size() == 0) lock.wait();
            if (queue.size() == maxSize) lock.notify();
            return queue.remove();
        }
    }
    
    public void offer(Object t) throws InterruptedException {
        synchronized (lock) {
            while (queue.size() == maxSize) lock.wait();
            if (queue.size() == 0) lock.notify();
            queue.add(t);
        }
    }
    
    public static void main(String[] args) {
        BlockingSizeQueue1 blockingSizeQueue = new BlockingSizeQueue1();

        new Thread(new Producer1(blockingSizeQueue), "生产者").start();
        new Thread(new Customer1(blockingSizeQueue), "消费者").start();
        new Thread(new Producer1(blockingSizeQueue), "生产者").start();
        new Thread(new Customer1(blockingSizeQueue), "消费者").start();
        new Thread(new Producer1(blockingSizeQueue), "生产者").start();
        new Thread(new Customer1(blockingSizeQueue), "消费者").start();
        new Thread(new Producer1(blockingSizeQueue), "生产者").start();
        new Thread(new Customer1(blockingSizeQueue), "消费者").start();
        new Thread(new Producer1(blockingSizeQueue), "生产者").start();
        new Thread(new Customer1(blockingSizeQueue), "消费者").start();
    }
}

class Producer1 implements Runnable {
    private BlockingSizeQueue1 queue;
    public Producer1(BlockingSizeQueue1 queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                queue.offer(UUID.randomUUID().toString());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Customer1  implements Runnable {
    private BlockingSizeQueue1 queue;
    public Customer1(BlockingSizeQueue1 queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}        这回终于没有NoSuchElementException!






你可能感兴趣的:(Java,基础)