java 生产者消费者_JAVA并发二十五:一篇文章,让你彻底弄懂生产者--消费者问题

208e0440851e90a6e3d16b296f0c3c2b.png

生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:

  1. 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据;

在实现生产者消费者问题时,可以采用三种方式:

1.使用Object的wait/notify的消息通知机制;

2.使用Lock的Condition的await/signal的消息通知机制;

3.使用BlockingQueue实现。本文主要将这三种实现方式进行总结归纳。

1. wait/notify的消息通知机制

1.1 预备知识

Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从wait()方法出返回,继续执行下面的操作。

  1. wait该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用wait()方法之后,当前线程会释放锁。如果调用wait()方法时,线程并未获取到锁的话,则会抛出IllegalMonitorStateException异常,这是以个RuntimeException。如果再次获取到锁的话,当前线程才能从wait()方法处成功返回。
  2. notify该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。该方法任意从WAITTING状态的线程中挑选一个进行通知,使得调用wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用wait()方法的线程能够从wait()方法处退出。调用notify后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。
  3. notifyAll该方法与 notify ()方法的工作方式相同,重要的一点差异是:notifyAll 使所有原来在该对象上 wait 的线程统统退出WAITTING状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。

1.2 wait/notify消息通知潜在的一些问题

1.notify早期通知

notify 通知的遗漏很容易理解,即 threadA 还没开始 wait 的时候,threadB 已经 notify 了,这样,threadB 通知是没有任何响应的,当 threadB 退出 synchronized 代码块后,threadA 再开始 wait,便会一直阻塞等待,直到被别的线程打断。比如在下面的示例代码中,就模拟出notify早期通知带来的问题:

public class EarlyNotify {    private static String lockObject = "";    public static void main(String[] args) {        WaitThread waitThread = new WaitThread(lockObject);        NotifyThread notifyThread = new NotifyThread(lockObject);        notifyThread.start();        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        waitThread.start();    }    static class WaitThread extends Thread {        private String lock;        public WaitThread(String lock) {            this.lock = lock;        }        @Override        public void run() {            synchronized (lock) {                try {                    System.out.println(Thread.currentThread().getName() + "  进去代码块");                    System.out.println(Thread.currentThread().getName() + "  开始wait");                    lock.wait();                    System.out.println(Thread.currentThread().getName() + "   结束wait");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }    static class NotifyThread extends Thread {        private String lock;        public NotifyThread(String lock) {            this.lock = lock;        }        @Override        public void run() {            synchronized (lock) {                System.out.println(Thread.currentThread().getName() + "  进去代码块");                System.out.println(Thread.currentThread().getName() + "  开始notify");                lock.notify();                System.out.println(Thread.currentThread().getName() + "   结束开始notify");            }        }    }}

示例中开启了两个线程,一个是WaitThread,另一个是NotifyThread。NotifyThread会先启动,先调用notify方法。然后WaitThread线程才启动,调用wait方法,但是由于通知过了,wait方法就无法再获取到相应的通知,因此WaitThread会一直在wait方法出阻塞,这种现象就是通知过早的现象。针对这种现象,解决方法,一般是添加一个状态标志,让waitThread调用wait方法前先判断状态是否已经改变了没,如果通知早已发出的话,WaitThread就不再去wait。对上面的代码进行更正:

public class EarlyNotify {    private static String lockObject = "";    private static boolean isWait = true;    public static void main(String[] args) {        WaitThread waitThread = new WaitThread(lockObject);        NotifyThread notifyThread = new NotifyThread(lockObject);        notifyThread.start();        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        waitThread.start();    }    static class WaitThread extends Thread {        private String lock;        public WaitThread(String lock) {            this.lock = lock;        }        @Override        public void run() {            synchronized (lock) {                try {                    while (isWait) {                        System.out.println(Thread.currentThread().getName() + "  进去代码块");                        System.out.println(Thread.currentThread().getName() + "  开始wait");                        lock.wait();                        System.out.println(Thread.currentThread().getName() + "   结束wait");                    }                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }    static class NotifyThread extends Thread {        private String lock;        public NotifyThread(String lock) {            this.lock = lock;        }        @Override        public void run() {            synchronized (lock) {                System.out.println(Thread.currentThread().getName() + "  进去代码块");                System.out.println(Thread.currentThread().getName() + "  开始notify");                lock.notifyAll();                isWait = false;                System.out.println(Thread.currentThread().getName() + "   结束开始notify");            }        }    }}

这段代码只是增加了一个isWait状态变量,NotifyThread调用notify方法后会对状态变量进行更新,在WaitThread中调用wait方法之前会先对状态变量进行判断,在该示例中,调用notify后将状态变量isWait改变为false,因此,在WaitThread中while对isWait判断后就不会执行wait方法,从而避免了Notify过早通知造成遗漏的情况。

总结:在使用线程的等待/通知机制时,一般都要配合一个 boolean 变量值(或者其他能够判断真假的条件),在 notify 之前改变该 boolean 变量的值,让 wait 返回后能够退出 while 循环(一般都要在 wait 方法外围加一层 while 循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在 wait 方法处。这样便保证了程序的正确性。

2.等待wait的条件发生变化

如果线程在等待时接受到了通知,但是之后等待的条件发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。

下面用一个例子来说明这种情况

public class ConditionChange {private static List lockObject = new ArrayList();public static void main(String[] args) {    Consumer consumer1 = new Consumer(lockObject);    Consumer consumer2 = new Consumer(lockObject);    Productor productor = new Productor(lockObject);    consumer1.start();    consumer2.start();    productor.start();}static class Consumer extends Thread {    private List lock;    public Consumer(List lock) {        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            try {                //这里使用if的话,就会存在wait条件变化造成程序错误的问题                if (lock.isEmpty()) {                    System.out.println(Thread.currentThread().getName() + " list为空");                    System.out.println(Thread.currentThread().getName() + " 调用wait方法");                    lock.wait();                    System.out.println(Thread.currentThread().getName() + "  wait方法结束");                }                String element = lock.remove(0);                System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}static class Productor extends Thread {    private List lock;    public Productor(List lock) {        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            System.out.println(Thread.currentThread().getName() + " 开始添加元素");            lock.add(Thread.currentThread().getName());            lock.notifyAll();        }    }}}会报异常:Exception in thread "Thread-1" Thread-0 list为空Thread-0 调用wait方法Thread-1 list为空Thread-1 调用wait方法Thread-2 开始添加元素Thread-1  wait方法结束java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

异常原因分析:在这个例子中一共开启了3个线程,Consumer1,Consumer2以及Productor。首先Consumer1调用了wait方法后,线程处于了WAITTING状态,并且将对象锁释放出来。因此,Consumer2能够获取对象锁,从而进入到同步代块中,当执行到wait方法时,同样的也会释放对象锁。因此,productor能够获取到对象锁,进入到同步代码块中,向list中插入数据后,通过notifyAll方法通知处于WAITING状态的Consumer1和Consumer2线程。consumer1得到对象锁后,从wait方法出退出,删除了一个元素让List为空,方法执行结束,退出同步块,释放掉对象锁。这个时候Consumer2获取到对象锁后,从wait方法退出,继续往下执行,这个时候Consumer2再执行lock.remove(0);就会出错,因为List由于Consumer1删除一个元素之后已经为空了。

解决方案:通过上面的分析,可以看出Consumer2报异常是因为线程从wait方法退出之后没有再次对wait条件进行判断,因此,此时的wait条件已经发生了变化。解决办法就是,在wait退出之后再对条件进行判断即可。

public class ConditionChange {private static List lockObject = new ArrayList();public static void main(String[] args) {    Consumer consumer1 = new Consumer(lockObject);    Consumer consumer2 = new Consumer(lockObject);    Productor productor = new Productor(lockObject);    consumer1.start();    consumer2.start();    productor.start();}static class Consumer extends Thread {    private List lock;    public Consumer(List lock) {        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            try {                //这里使用if的话,就会存在wait条件变化造成程序错误的问题                while (lock.isEmpty()) {                    System.out.println(Thread.currentThread().getName() + " list为空");                    System.out.println(Thread.currentThread().getName() + " 调用wait方法");                    lock.wait();                    System.out.println(Thread.currentThread().getName() + "  wait方法结束");                }                String element = lock.remove(0);                System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}static class Productor extends Thread {    private List lock;    public Productor(List lock) {        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            System.out.println(Thread.currentThread().getName() + " 开始添加元素");            lock.add(Thread.currentThread().getName());            lock.notifyAll();        }    }}}

上面的代码与之前的代码仅仅只是将 wait 外围的 if 语句改为 while 循环即可,这样当 list 为空时,线程便会继续等待,而不会继续去执行删除 list 中元素的代码。

总结:在使用线程的等待/通知机制时,一般都要在 while 循环中调用 wait()方法,因此xuy配合使用一个 boolean 变量(或其他能判断真假的条件,如本文中的 list.isEmpty()),满足 while 循环的条件时,进入 while 循环,执行 wait()方法,不满足 while 循环的条件时,跳出循环,执行后面的代码。

3. “假死”状态

现象:如果是多消费者和多生产者情况,如果使用notify方法可能会出现“假死”的情况,即唤醒的是同类线程。

原因分析:假设当前多个生产者线程会调用wait方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify通知处于WAITTING状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。

解决办法:将notify方法替换成notifyAll方法,如果使用的是lock的话,就将signal方法替换成signalAll方法。

总结

在Object提供的消息通知机制应该遵循如下这些条件:

  1. 永远在while循环中对条件进行判断而不是if语句中进行wait条件的判断;
  2. 使用NotifyAll而不是使用notify。

基本的使用范式如下:

// The standard idiom for calling the wait method in Java synchronized (sharedObject) {     while (condition) {     sharedObject.wait();         // (Releases lock, and reacquires on wakeup)     }     // do action based upon condition e.g. take or put into queue }

1.3 wait/notifyAll实现生产者-消费者

利用wait/notifyAll实现生产者和消费者代码如下:

public class ProductorConsumer {public static void main(String[] args) {    LinkedList linkedList = new LinkedList();    ExecutorService service = Executors.newFixedThreadPool(15);    for (int i = 0; i  list;    private int maxLength;    public Productor(List list, int maxLength) {        this.list = list;        this.maxLength = maxLength;    }    @Override    public void run() {        while (true) {            synchronized (list) {                try {                    while (list.size() == maxLength) {                        System.out.println("生产者" + Thread.currentThread().getName() + "  list以达到最大容量,进行wait");                        list.wait();                        System.out.println("生产者" + Thread.currentThread().getName() + "  退出wait");                    }                    Random random = new Random();                    int i = random.nextInt();                    System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i);                    list.add(i);                    list.notifyAll();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}static class Consumer implements Runnable {    private List list;    public Consumer(List list) {        this.list = list;    }    @Override    public void run() {        while (true) {            synchronized (list) {                try {                    while (list.isEmpty()) {                        System.out.println("消费者" + Thread.currentThread().getName() + "  list为空,进行wait");                        list.wait();                        System.out.println("消费者" + Thread.currentThread().getName() + "  退出wait");                    }                    Integer element = list.remove(0);                    System.out.println("消费者" + Thread.currentThread().getName() + "  消费数据:" + element);                    list.notifyAll();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}}输出结果:生产者pool-1-thread-1 生产数据-232820990生产者pool-1-thread-1 生产数据1432164130生产者pool-1-thread-1 生产数据1057090222生产者pool-1-thread-1 生产数据1201395916生产者pool-1-thread-1 生产数据482766516生产者pool-1-thread-1  list以达到最大容量,进行wait消费者pool-1-thread-15  退出wait消费者pool-1-thread-15  消费数据:1237535349消费者pool-1-thread-15  消费数据:-1617438932消费者pool-1-thread-15  消费数据:-535396055消费者pool-1-thread-15  消费数据:-232820990消费者pool-1-thread-15  消费数据:1432164130消费者pool-1-thread-15  消费数据:1057090222消费者pool-1-thread-15  消费数据:1201395916消费者pool-1-thread-15  消费数据:482766516消费者pool-1-thread-15  list为空,进行wait生产者pool-1-thread-5  退出wait生产者pool-1-thread-5 生产数据1442969724生产者pool-1-thread-5 生产数据1177554422生产者pool-1-thread-5 生产数据-133137235生产者pool-1-thread-5 生产数据324882560生产者pool-1-thread-5 生产数据2065211573生产者pool-1-thread-5 生产数据253569900生产者pool-1-thread-5 生产数据571277922生产者pool-1-thread-5 生产数据1622323863生产者pool-1-thread-5  list以达到最大容量,进行wait消费者pool-1-thread-10  退出wait

2. 使用Lock中Condition的await/signalAll实现生产者-消费者

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

针对wait方法

void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;

long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;

boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位

boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

针对notify方法

void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。

void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

也就是说wait--->await,notify---->Signal。

如果采用lock中Conditon的消息通知原理来实现生产者-消费者问题,原理同使用wait/notifyAll一样。直接上代码:

public class ProductorConsumer {private static ReentrantLock lock = new ReentrantLock();private static Condition full = lock.newCondition();private static Condition empty = lock.newCondition();public static void main(String[] args) {    LinkedList linkedList = new LinkedList();    ExecutorService service = Executors.newFixedThreadPool(15);    for (int i = 0; i  list;    private int maxLength;    private Lock lock;    public Productor(List list, int maxLength, Lock lock) {        this.list = list;        this.maxLength = maxLength;        this.lock = lock;    }    @Override    public void run() {        while (true) {            lock.lock();            try {                while (list.size() == maxLength) {                    System.out.println("生产者" + Thread.currentThread().getName() + "  list以达到最大容量,进行wait");                    full.await();                    System.out.println("生产者" + Thread.currentThread().getName() + "  退出wait");                }                Random random = new Random();                int i = random.nextInt();                System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i);                list.add(i);                empty.signalAll();            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                lock.unlock();            }        }    }}static class Consumer implements Runnable {    private List list;    private Lock lock;    public Consumer(List list, Lock lock) {        this.list = list;        this.lock = lock;    }    @Override    public void run() {        while (true) {            lock.lock();            try {                while (list.isEmpty()) {                    System.out.println("消费者" + Thread.currentThread().getName() + "  list为空,进行wait");                    empty.await();                    System.out.println("消费者" + Thread.currentThread().getName() + "  退出wait");                }                Integer element = list.remove(0);                System.out.println("消费者" + Thread.currentThread().getName() + "  消费数据:" + element);                full.signalAll();            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                lock.unlock();            }        }    }}}输出结果:消费者pool-1-thread-9  消费数据:1146627506消费者pool-1-thread-9  消费数据:1508001019消费者pool-1-thread-9  消费数据:-600080565消费者pool-1-thread-9  消费数据:-1000305429消费者pool-1-thread-9  消费数据:-1270658620消费者pool-1-thread-9  消费数据:1961046169消费者pool-1-thread-9  消费数据:-307680655消费者pool-1-thread-9  list为空,进行wait消费者pool-1-thread-13  退出wait消费者pool-1-thread-13  list为空,进行wait消费者pool-1-thread-10  退出wait生产者pool-1-thread-5  退出wait生产者pool-1-thread-5 生产数据-892558288生产者pool-1-thread-5 生产数据-1917220008生产者pool-1-thread-5 生产数据2146351766生产者pool-1-thread-5 生产数据452445380生产者pool-1-thread-5 生产数据1695168334生产者pool-1-thread-5 生产数据1979746693生产者pool-1-thread-5 生产数据-1905436249生产者pool-1-thread-5 生产数据-101410137生产者pool-1-thread-5  list以达到最大容量,进行wait生产者pool-1-thread-1  退出wait生产者pool-1-thread-1  list以达到最大容量,进行wait生产者pool-1-thread-4  退出wait生产者pool-1-thread-4  list以达到最大容量,进行wait生产者pool-1-thread-2  退出wait生产者pool-1-thread-2  list以达到最大容量,进行wait生产者pool-1-thread-3  退出wait生产者pool-1-thread-3  list以达到最大容量,进行wait消费者pool-1-thread-9  退出wait消费者pool-1-thread-9  消费数据:-892558288

3. 使用BlockingQueue实现生产者-消费者

由于BlockingQueue内部实现就附加了两个阻塞操作。即当队列已满时,阻塞向队列中插入数据的线程,直至队列中未满;当队列为空时,阻塞从队列中获取数据的线程,直至队列非空时为止。可以利用BlockingQueue实现生产者-消费者为题,阻塞队列完全可以充当共享数据区域,就可以很好的完成生产者和消费者线程之间的协作。

public class ProductorConsumer {    private static LinkedBlockingQueue queue = new LinkedBlockingQueue<>();    public static void main(String[] args) {        ExecutorService service = Executors.newFixedThreadPool(15);        for (int i = 0; i 

可以看出,使用BlockingQueue来实现生产者-消费者很简洁,这正是利用了BlockingQueue插入和获取数据附加阻塞操作的特性。

关于生产者-消费者实现的三中方式,到这里就全部总结出来,如果觉得不错的话,请点赞,也算是给我的鼓励,在此表示感谢!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值