前面介绍了多线程并发之时的资源抢占状况,以及利用同步、加锁、信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的来龙去脉。平常生活中,常常存在两个先后关联的事务,像雇员和雇主这两个角色,他们之间的某些工做就带有因果关系。好比要等雇主接到了项目,雇员才有活干;又如每个月末员工都等着老板发工资,这样才有钱逛街和吃大餐,此时员工的消费行为便依赖于老板的发薪水动做。如此看来,两个线程之间理应创建某种消息通路,每当线程A完成某个事项,就将完成标志通知线程B,线程B收到通知以后,认为前提条件已经知足,这才进行后续的处理过程。线程之间的消息通路,可视做在线程间传递信息,专业的说法叫作“通讯”,如何在多线程并发时进行有效通讯,这是多线程技术中的一大课题。
依据线程并发时的不一样管理机制,线程间的通讯也各有不一样的方式,接下来将分别论述同步机制与加锁机制之下的两种线程通讯过程。
首先是同步机制,采用同步代码块的话,须要在关键字synchronized后面补充待同步的对象实例,以前的同步代码块统一写成“synchronized (this)”。但是圆括号内部必定要填this吗?圆括号的内部参数到底是干什么用的?其实synchronized附带的圆括号参数正是在线程间通讯的邮差,之前的同步演示代码因为没进行线程通讯,所以圆括号里的参数没有具体要求,通常填this便可。如今要想在线程间进行通讯,就必须启用圆括号参数了,而且两个线程都要在synchronized后面填写该参数对象。
举个例子,雇员等着雇主发工资,那员工怎样才知道老板已经发了呢?要是由员工本身一下子一下子去查银行卡,平时的工做都会受到影响,因此可以让员工留个等工资的心眼就好。而后老板一个一个发工资,发完以后给员工递个工资条,或者给员工发封工资邮件,这样员工收到工资条便知薪水到帐了。那么在等工资和发工资这两个线程之间,便可令工资条做为两者的信使,因而同步代码块可改写为“synchronized (工资条对象)”的形式。同时工资条对象还要支持等待与发放两个动做,由于这类动做早就隐藏在Object类的基本方法中,因此开发者没必要担忧工资条对象该为Integer类型仍是别的什么类型,凡是正常的实例都拥有等待与发放的方法,具体的方法说明以下:
wait:等待通知。
notify:在等待队列中随机挑选一个线程发放通知。
notifyAll:向等待队列中的全部线程发放通知。
在编码实现同步机制的通讯过程时,先分别建立雇员和雇主的工做任务,其中雇员任务在同步代码块中调用工资条对象的wait方法,表示等着发工资;而雇主任务在同步代码块中调用工资条对象的notify方法,表示发完工资了。而后依次启动员工线程和老板线程,员工线程负责等工资以及收到工资后的消费行为,老板线程负责发工资以及记帐操做。据此编写的同步线程通讯代码示例以下:html
// 员工与老板之间经过工资条通讯 private static Integer salary = 5000; // 测试经过wait和notify方法进行线程间通讯 private static void testWaitNotify() { // 建立雇员的工做任务 Runnable employee = new Runnable() { @Override public void run() { PrintUtils.print(Thread.currentThread().getName(), "等着发工资。"); synchronized (salary) { // 工资是个人,大家别抢 try { salary.wait(); // 等待发工资 // 打印拿到工资后的庆祝日志 PrintUtils.print(Thread.currentThread().getName(), "今晚赶忙吃大餐。"); } catch (InterruptedException e) { // 等待期间容许接收中断信号 e.printStackTrace(); } } } }; // 建立雇主的工做任务 Runnable boss = new Runnable() { @Override public void run() { // 稍等一下子,老板线程的同步代码块务必在员工线程的同步代码块以后开始运行,不然员工线程将一直等待 wait_a_moment(); PrintUtils.print(Thread.currentThread().getName(), "开始发工资。"); synchronized (salary) { // 由我发工资,大家别闹 wait_a_moment(); // 银行转帐也须要时间 salary.notify(); // 随机通知其中一个等待线程 // 手好酸,发工资也是个体力活,记个帐 PrintUtils.print(Thread.currentThread().getName(), "发完工资了。"); } } }; new Thread(employee, "同步机制的员工").start(); // 启动员工等工资的线程 new Thread(boss, "同步机制的老板").start(); // 启动老板发工资的线程 } // 稍等一下子,模拟平常事务的时间消耗 private static void wait_a_moment() { int delay = new Random().nextInt(500); // 生成500之内的随机整数 try { Thread.sleep(delay); // 睡眠若干毫秒 } catch (InterruptedException e) { } }
运行上面的线程通讯代码,打印出如下的线程日志:多线程
14:37:29.685 同步机制的员工 等着发工资。 14:37:29.994 同步机制的老板 开始发工资。 14:37:30.120 同步机制的老板 发完工资了。 14:37:30.120 同步机制的员工 今晚赶忙吃大餐。
从日志可见,员工线程果真在等到工资以后才去吃大餐。并发
同步机制可以经过wait/notify完成线程通讯功能,那么加锁机制又该如何进行线程间通讯呢?既然加锁机制设计了专门的锁工具,那么锁钥内外的线程也只能经过锁工具来通讯,信使则为调用锁对象的newCondition方法返回的Condition条件对象。条件对象一样拥有等待与发放的方法,且与Object类的三个方法一一对应,具体说明以下:
await:等待通知。
signal:在等待队列中随机挑选一个线程发放通知。
signalAll:向等待队列中的全部线程发放通知。
以可重入锁ReentrantLock为例,依然要先分别建立雇员和雇主的工做任务,其中雇员任务在加锁以后再调用条件对象的await方法,表示等着发工资;而雇主任务在加锁以后再调用条件对象的signal方法,表示发完工资了;另外雇员任务和雇主任务均需在结束以前进行解锁。而后依次启动员工线程和老板线程,员工线程负责等工资以及收到工资后的消费行为,老板线程负责发工资以及记帐操做。下面是在加解锁线程之间进行通讯的代码例子:dom
// 建立一个可重入锁 private final static ReentrantLock reentrantLock = new ReentrantLock(); // 获取可重入锁的条件对象 private static Condition condition = reentrantLock.newCondition(); // 测试经过Condition对象进行线程间通讯 private static void testCondition() { // 建立雇员的工做任务 Runnable employee = new Runnable() { @Override public void run() { PrintUtils.print(Thread.currentThread().getName(), "等着发工资。"); reentrantLock.lock(); // 对可重入锁加锁 try { condition.await(); // 这里在等待条件对象的信号 // 打印拿到工资后的庆祝日志 PrintUtils.print(Thread.currentThread().getName(), "今晚赶忙吃大餐。"); } catch (InterruptedException e) { // 等待期间容许接收中断信号 e.printStackTrace(); } reentrantLock.unlock(); // 对可重入锁解锁 } }; // 建立雇主的工做任务 Runnable boss = new Runnable() { @Override public void run() { // 稍等一下子,老板线程的加锁务必在员工线程的加锁以后执行,不然员工线程将一直等待 wait_a_moment(); PrintUtils.print(Thread.currentThread().getName(), "开始发工资。"); reentrantLock.lock(); // 对可重入锁加锁 wait_a_moment(); // 银行转帐也须要时间 condition.signal(); // 给条件对象发送信号 // 手好酸,发工资也是个体力活,记个帐 PrintUtils.print(Thread.currentThread().getName(), "发完工资了。"); reentrantLock.unlock(); // 对可重入锁解锁 } }; new Thread(employee, "加锁机制的员工").start(); // 启动员工等工资的线程 new Thread(boss, "加锁机制的老板").start(); // 启动老板发工资的线程 }
运行上述的线程通讯代码,打印出以下的线程日志:ide
14:57:07.794 加锁机制的员工 等着发工资。 14:57:07.801 加锁机制的老板 开始发工资。 14:57:07.905 加锁机制的老板 发完工资了。 14:57:07.906 加锁机制的员工 今晚赶忙吃大餐。
可见加锁机制一样实现了线程间通讯的功能。工具
更多Java技术文章参见《Java开发笔记(序)章节目录》测试