前面介绍了多线程并发之时的资源抢占情况,以及利用同步、加锁、信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果。日常生活中,经常存在两个前后关联的事务,像雇员和雇主这两个角色,他们之间的某些工作就带有因果关系。比如要等雇主接到了项目,雇员才有活干;又如每月末员工都等着老板发工资,这样才有钱逛街和吃大餐,此时员工的消费行为便依赖于老板的发薪水动作。如此看来,两个线程之间理应建立某种消息通路,每当线程A完成某个事项,就将完成标志通知线程B,线程B收到通知之后,认为前提条件已经满足,这才进行后续的处理过程。线程之间的消息通路,可视作在线程间传递信息,专业的说法叫做“通信”,如何在多线程并发时进行有效通信,这是多线程技术中的一大课题。
依据线程并发时的不同管理机制,线程间的通信也各有不同的方式,接下来将分别论述同步机制与加锁机制之下的两种线程通信过程。
首先是同步机制,采用同步代码块的话,需要在关键字synchronized后面补充待同步的对象实例,之前的同步代码块统一写成“synchronized (this)”。可是圆括号内部一定要填this吗?圆括号的内部参数究竟是干什么用的?其实synchronized附带的圆括号参数正是在线程间通信的邮差,以前的同步演示代码由于没进行线程通信,因此圆括号里的参数没有具体要求,一般填this即可。现在要想在线程间进行通信,就必须启用圆括号参数了,并且两个线程都要在synchronized后面填写该参数对象。
举个例子,雇员等着雇主发工资,那员工怎样才知道老板已经发了呢?要是由员工自己一会儿一会儿去查银行卡,平时的工作都会受到影响,所以可让员工留个等工资的心眼就好。然后老板一个一个发工资,发完之后给员工递个工资条,或者给员工发封工资邮件,这样员工收到工资条便知薪水到账了。那么在等工资和发工资这两个线程之间,即可令工资条作为二者的信使,于是同步代码块可改写为“synchronized (工资条对象)”的形式。同时工资条对象还要支持等待与发放两个动作,因为这类动作早就隐藏在Object类的基本方法中,所以开发者不必担心工资条对象该为Integer类型还是别的什么类型,凡是正常的实例都拥有等待与发放的方法,具体的方法说明如下:
wait:等待通知。
notify:在等待队列中随机挑选一个线程发放通知。
notifyAll:向等待队列中的所有线程发放通知。
在编码实现同步机制的通信过程时,先分别创建雇员和雇主的工作任务,其中雇员任务在同步代码块中调用工资条对象的wait方法,表示等着发工资;而雇主任务在同步代码块中调用工资条对象的notify方法,表示发完工资了。然后依次启动员工线程和老板线程,员工线程负责等工资以及收到工资后的消费行为,老板线程负责发工资以及记账操作。据此编写的同步线程通信代码示例如下:

	// 员工与老板之间通过工资条通信
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方法,表示发完工资了;另外雇员任务和雇主任务均需在结束之前进行解锁。然后依次启动员工线程和老板线程,员工线程负责等工资以及收到工资后的消费行为,老板线程负责发工资以及记账操作。下面是在加解锁线程之间进行通信的代码例子:

	// 创建一个可重入锁
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(); // 启动老板发工资的线程
}

运行上述的线程通信代码,打印出如下的线程日志:

14:57:07.794 加锁机制的员工 等着发工资。
14:57:07.801 加锁机制的老板 开始发工资。
14:57:07.905 加锁机制的老板 发完工资了。
14:57:07.906 加锁机制的员工 今晚赶紧吃大餐。

可见加锁机制同样实现了线程间通信的功能。

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(一百零三)线程间的通信方式的更多相关文章

  1. Java开发笔记(九十三)深入理解字节缓存

    前面介绍了文件通道的读写操作,其中用到字节缓存ByteBuffer,它是位于通道内部的存储空间,也是通道唯一可用的存储形式.ByteBuffer有两种构建方式,一种是调用静态方法wrap,根据输入的字 ...

  2. Java开发笔记(一百零四)普通线程池的运用

    前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...

  3. Java开发笔记(一百零五)几种定时器线程池

    前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...

  4. Java开发笔记(一百零六)Fork+Join框架实现分而治之

    前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...

  5. Java开发笔记(一百零二)信号量的请求与释放

    前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:1.某块代码被加锁之后,对其它线程而言就处于繁忙状态,缺乏弹性的阈值范围:2 ...

  6. Java开发笔记(一百零一)通过加解锁避免资源冲突

    前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...

  7. Java开发笔记(一百)线程同步synchronized

    多个线程一起办事固然能够加快处理速度,但是也带来一个问题:两个线程同时争抢某个资源时该怎么办?看来资源共享的另一面便是资源冲突,正所谓鱼与熊掌不可兼得,系统岂能让多线程这项技术专占好处?果然是有利必有 ...

  8. Java开发笔记(一百零九)XML报文的定义和解析

    前面介绍了JSON格式的报文解析,虽然json串短小精悍,也能有效表达层次结构,但是每个元素只能找到对应的元素值,不能体现更丰富的样式特征.比如某个元素除了要传输它的字符串文本,还想传输该文本的类型. ...

  9. Java开发笔记(一百三十)Swing的选择框

    不管是AWT还是Swing,都把选择框分成两类:复选框和单选按钮,这两类控件无论是外观上还是功能上均有显著差异.例如,在外观方面,复选框是在方框内打勾,而单选按钮是在圆圈内画圆点:在功能方面,复选框允 ...

随机推荐

  1. PHP的抽象类、接口的区别和选择

    1.对接口的使用是通过关键字implements.对抽象类的使用是通过关键字extends.当然接口也可以通过关键字extends继承. 2.接口中不可以声明成员变量(包括类静态变量),但是可以声明类 ...

  2. opencv中的仿射变换

    什么是仿射变换? 原理:1.一个任意的仿射变换都能表示为 乘以一个矩阵(线性变换) 接着再 加上一个向量(平移) 2.综上所述,我们能够用仿射变换来表示: 1)旋转(线性变换) 2)平移(向量加) 3 ...

  3. 03017_ajax

    1.Ajax概述 (1)什么是同步,什么是异步? ①同步现象:客户端发送请求到服务器端,当服务器返回响应之前,客户端都处于等待卡死状态: ②异步现象:客户端发送请求到服务器端,无论服务器是否返回响应, ...

  4. {{}},ng-bind和ng-model的区别

    ng-bind 与ng-model区别 <input ng-model="object.xxx"> <span ng-bind="object.xxx& ...

  5. mysql查询当天的数据

    mysql查询当天的数据 贴代码: #两个时间都使用to_days()函数 select * from reple where to_days(create_time) = to_days(NOW() ...

  6. ajax跨域请求的处理

    跨域的情形有很多种,网上有人给出了一份表格, 表格中标识为”不允许”通信的情况都属于跨域.实际网络服务中需要跨域的情况确实存在,于是开发者们提供了一种解决方案,就是使用jsonp格式进行数据交互,它不 ...

  7. logback mybatis 打印sql语句

    logbac.xml 文件的基础配置参考的园友的 http://www.cnblogs.com/yuanermen/archive/2012/02/13/2349609.html 然后hibernat ...

  8. Intellij IDEA快捷键大全

    Intellij IDEA快捷键大全 Intellij IDEA这个工具有些方面确实比较优秀,使用了一段时间的IntelliJ IDEA,感觉这个JAVA IDE非常好用!比如javascript自动 ...

  9. 图说不为人知的IT传奇故事-3-硅谷DNA创造者HP

    此系列文章为“图说不为人知的IT传奇故事”,各位大忙人可以在一分钟甚至几秒内了解把握整个内容,真可谓“大忙人的福利”呀!!希望各位IT界的朋友在钻研技术的同时,也能在文学.历史上有所把握.了解这些故事 ...

  10. C语言编程题002

    给出两个整数,L和R,其中L<=A<=B<=R,然后求出A^B值最大的数.其中1<=L<=R<=1000. 比如说L = 1;R = 3; L 0001 R 001 ...