多线程间的通信
本文继续接着讲解多线程,这儿会涉及到多线程的高级部分——多线程间的通信。
多线程间的通信其实就是多个线程都在处理同一个资源,但是处理的任务却不一样。最经典的案例就是生产者和消费者的案例了,下面我就来演示该案例。
单个生产者和消费者
我将采用循序渐进的方式来演示单个生产者和消费者的案例,以供大家参考。首先,我们可能会写出这样的案例代码,如下:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 提供了给商品赋值的方法
public void set(String name)
{
this.name = name + "--" + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
}
// 提供获取一个商品的方法
public void get()
{
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name);
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
有心的同学运行以上程序,可能会出现没生产就消费的问题,为了解决该问题,我们可以使用同步,可将Res类的set()和get()方法置为同步函数,改完之后的案例代码为:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 提供了给商品赋值的方法
public synchronized void set(String name)
{
this.name = name + "--" + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
}
// 提供获取一个商品的方法
public synchronized void get()
{
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name);
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
这样改完之后,有心的同学运行以上程序,又可能出现了连续的生产没有消费的情况,和需求生产一个消费一个的情况不符。为了解决此问题,这里使用了等待唤醒机制,下面这些方法我们是必须知道的。
- wait():该方法可以让线程处于冻结状态,并将线程临时存储到线程池中。
- notify():唤醒指定线程池中的任意一个线程。
- notifyAll():唤醒指定线程池中的所有线程。
wait()/notify()/notifyAll():都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
思考这样一个问题:wait()、notify()、notifyAll()等方法用来操作线程,为什么定义在了Object类中?
答:
- 这些方法必须使用在同步中,因为它们是用来操作同步锁上的线程的状态的。
- 同时在使用这些方法时,必须标识它们所属于的锁,标识方式就是:
锁对象.wait();
、锁对象.notify();
、锁对象.notifyAll();
。相同锁的notify(),可以获取相同锁的wait()。 - 锁可以是任意对象,所以任意对象调用的方法一定定义在Object类中。
因为这些方法在操作同步中线程时,都必须标识它们所操作线程持有的锁,只有同一个锁上的被等待线程,才可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
再思考这样一个问题:wait()、sleep()方法有什么区别?
答:
- 相同:可以让线程处于冻结状态。
- 不同点:
- wait()可以指定时间,也可以不指定;sleep()必须指定时间。
- wait()释放CPU资源,释放锁;sleep()释放CPU资源,不释放锁。
了解以上知识点之后,单个生产者和消费者案例的最终代码就写为:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public synchronized void set(String name)
{
if (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{this.wait();}catch(InterruptedException e){ }
this.name = name + "--" + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
// 生产完毕,将标记改为true。
flag = true;
// 唤醒消费者。
this.notify();
}
// 提供获取一个商品的方法
public synchronized void get()
{
if (!flag)
try{this.wait();}catch(InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name);
// 将标记改为false。
flag = false;
// 唤醒生产者
this.notify();
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
单个生产者和消费者的案例就讲到这儿,下面就要讲解多个生产者和消费者的案例了。
多个生产者和消费者
首先,我给出如下程序:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public synchronized void set(String name) //
{
if (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{this.wait();}catch(InterruptedException e){ }
this.name = name + "--" + count; // 面包1、面包2、面包3
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
// 生产完毕,将标记改为true。
flag = true;
// 唤醒消费者。
this.notify();
}
// 提供获取一个商品的方法
public synchronized void get()
{
if (!flag)
try{this.wait();}catch(InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name);
// 将标记改为false。
flag = false;
// 唤醒生产者
this.notify();
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo3
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
从以上程序代码中我们可以看到有两个生产者线程和两个消费者线程,即多个生产者和多个消费者。运行以上代码,可以很清楚地看到所产生的问题——重复生产或重复消费。我经过复杂的分析,发现被唤醒的线程没有判断标记就开始工作(生产or消费)了,从而导致了重复的生产和消费的发生。那么如何来解决这个问题呢?很简单,那就是被唤醒的线程必须判断标记(可以使用while循环搞定)。如此一来,经过修改后的程序代码就变为:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public synchronized void set(String name) //
{
while (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{this.wait();}catch(InterruptedException e){ }
this.name = name + "--" + count; // 面包1、面包2、面包3
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
// 生产完毕,将标记改为true。
flag = true;
// 唤醒消费者。
this.notify();
}
// 提供获取一个商品的方法
public synchronized void get()
{
while (!flag)
try{this.wait();}catch(InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name);
// 将标记改为false。
flag = false;
// 唤醒生产者
this.notify();
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo3
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
修改完之后,我们再次运行以上程序,又会产生一个问题,那就是死锁了,所有的线程都处于冻结状态。究其原因是本方线程在唤醒时,又一次唤醒了本方线程,而本方线程循环判断标记,又继续等待,而导致所有的线程都等待了。那又该如何解决这个问题呢?解决手段很明显,即本方线程如果唤醒了对方线程,就可以解决了(可以使用notifyAll()方法)。大家可能有疑问,那不是全唤醒了吗?既有本方线程,又有对方线程,但是本方线程醒后会判断标记,继续等待,这样对方就有线程可以执行了。如此一来,多个生产者和消费者案例的代码就要修改为:
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public synchronized void set(String name) //
{
while (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{this.wait();}catch(InterruptedException e){ } // t0(等待)、t1(等待)
this.name = name + "--" + count; // 面包1、面包2、面包3
count++; // 4
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); // t0、面包1 t0、面包2、t1、面包3
// 生产完毕,将标记改为true。
flag = true;
// 唤醒所有等待线程(包括本方线程)。
this.notifyAll();
}
// 提供获取一个商品的方法
public synchronized void get() // t3
{
while (!flag)
try{this.wait();}catch(InterruptedException e){ } // t2(等待)、t3(等待)
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name); // t2、面包1
// 将标记改为false。
flag = false;
// 唤醒所有等待线程(包括本方线程)。
this.notifyAll();
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class ProducerConsumerDemo3
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
这样就已经实现了多生产多消费,但是还有些小问题,那就是效率有点低,因为notifyAll()方法也唤醒了本方线程,做了不必要的判断。为了解决这个效率有点低的问题,JDK1.5版本中提供了多线程的升级解决方案。下面就来为大家讲解哟!
多线程的升级解决方案
这里我也是采用循序渐进的方式来讲解JDK1.5版本中提供的多线程升级解决方案,希望能更加容易地让大家接受。
为了解决多生产多消费的效率低下这一核心问题,在这儿我就告诉大家势必要用到JDK1.5中java.util.concurrent.locks包中的对象,其中就有Lock接口。须知同步代码块或者同步函数的锁操作是隐式的,而JDK1.5中出现的Lock接口按照面向对象的思想,将锁单独封装成了一个对象,并提供了对锁的显示操作,诸如以下操作:
lock()
:获取锁。unlock()
:释放锁。
总而言之,Lock接口的出现比synchronized有了更多的操作,它就是同步的替代。所以,首先,我们将多个生产者和消费者案例中的同步更换为Lock接口的形式,代码如下,供大家参考。
import java.util.concurrent.locks.*;
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 创建新Lock
private Lock lock = new ReentrantLock();
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public void set(String name) //
{
// 获取锁。
lock.lock();
try
{
while (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{this.wait();}catch(InterruptedException e){ } // t0(等待)、t1(等待)
this.name = name + "--" + count; // 面包1、面包2、面包3
count++; // 4
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); // t0、面包1 t0、面包2、t1、面包3
// 生产完毕,将标记改为true。
flag = true;
// 唤醒所有等待线程(包括本方线程)。
this.notifyAll();
}
finally
{
// 释放锁。
lock.unlock();
}
}
// 提供获取一个商品的方法
public void get() // t3
{
lock.lock();
try
{
while (!flag)
try{this.wait();}catch(InterruptedException e){ } // t2(等待)、t3(等待)
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name); // t2、面包1
// 将标记改为false。
flag = false;
// 唤醒所有等待线程(包括本方线程)。
this.notifyAll();
}
finally
{
lock.unlock();
}
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class NewProducerConsumerDemo
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
替换完之后,发现运行以上程序会报告如下异常,我截图如下。
究其原因就是wait()没有了同步区域,没有了所属的同步锁。同步升级了,其中锁已经不再是任意对象了,而是Lock类型的对象,那么和任意对象绑定的监视器方法,是不是也升级了,有了专门和Lock类型锁的绑定的监视器方法呢?通过查阅API帮助文档,可知Condition接口替代了Object类中的监视器方法。以前是监视器方法封装到了每一个对象中,现在是将监视器方法封装到了Condition对象中,方法名为await()、signal()、signalAll()。那么问题又来了,监视器对象Condition如何和Lock绑定呢?答案是可以通过Lock接口的newCondition()方法完成。
按照上面的分析,我将多个生产者和消费者案例的代码修改如下:
import java.util.concurrent.locks.*;
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 创建新Lock
private Lock lock = new ReentrantLock();
// 创建和Lock接口绑定的监视器对象
private Condition con = lock.newCondition();
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public void set(String name) //
{
// 获取锁。
lock.lock();
try
{
while (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{con.await();}catch(InterruptedException e){ } // t0(等待)、t1(等待)
this.name = name + "--" + count; // 面包1、面包2、面包3
count++; // 4
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); // t0、面包1 t0、面包2、t1、面包3
// 生产完毕,将标记改为true。
flag = true;
// 唤醒所有等待线程(包括本方线程)。
// this.notifyAll();
con.signalAll();
}
finally
{
// 释放锁。
lock.unlock();
}
}
// 提供获取一个商品的方法
public void get() // t3
{
lock.lock();
try
{
while (!flag)
try{con.await();}catch(InterruptedException e){ } // t2(等待)、t3(等待)
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name); // t2、面包1
// 将标记改为false。
flag = false;
// 唤醒所有等待线程(包括本方线程)。
// this.notifyAll();
con.signalAll();
}
finally
{
lock.unlock();
}
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class NewProducerConsumerDemo
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
改完,运行以上程序,虽然运行没问题,但是问题依旧,一样唤醒了本方线程,效率仍旧低下!接下来我们就要看看如何真正解决多生产多消费的效率低问题了。
经过上面一步一步地分析,到这里,我们差不多可以写出真正解决多生产多消费效率低问题的程序了,现将代码贴出如下,以供大家参考。
import java.util.concurrent.locks.*;
// 描述资源
class Res
{
private String name; // 资源名称
private int count = 1; // 资源编号
// 创建新Lock
private Lock lock = new ReentrantLock();
// 创建和Lock接口绑定的监视器对象。创建两个。
// 生产者监视器
private Condition producer_con = lock.newCondition();
// 消费者监视器
private Condition consumer_con = lock.newCondition();
// 定义标记。
private boolean flag;
// 提供了给商品赋值的方法
public void set(String name) //
{
// 获取锁。
lock.lock();
try
{
while (flag) // 判断标记为true,就执行wait()等待。为false,就生产。
try{producer_con.await();}catch(InterruptedException e){ } // t0(等待)、t1(等待)
this.name = name + "--" + count; // 面包1、面包2、面包3
count++; // 4
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); // t0、面包1 t0、面包2、t1、面包3
// 生产完毕,将标记改为true。
flag = true;
// 生产完毕,应该唤醒一个消费者来消费。
consumer_con.signal();
}
finally
{
// 释放锁。
lock.unlock();
}
}
// 提供获取一个商品的方法
public void get() // t3
{
lock.lock();
try
{
while (!flag)
try{consumer_con.await();}catch(InterruptedException e){ } // t2(等待)、t3(等待)
System.out.println(Thread.currentThread().getName() + ".......消费者......." + this.name); // t2、面包1
// 将标记改为false。
flag = false;
// 消费完后,应该唤醒一个生产者来生产。
producer_con.signal();
}
finally
{
lock.unlock();
}
}
}
// 生成者
class Producer implements Runnable
{
private Res r;
Producer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.set("面包");
}
}
// 消费者
class Consumer implements Runnable
{
private Res r;
Consumer(Res r)
{
this.r = r;
}
public void run()
{
while (true)
r.get();
}
}
class NewProducerConsumerDemo
{
public static void main(String[] args)
{
// 1、创建资源
Res r = new Res();
// 2、创建两个任务。
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
// 3、创建线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}