【Java并发学习】之线程合作
前言
在前面的小节中,我们谈到了线程之间有两种主要的关系,一种是竞争关系,另外一种是合作关系,竞争关系的基本概念以及基本操作我们已经在前面一个小节中有所学习,本小节呢,主要学习的是线程之间的合作,通过协调过多个线程之间的关系,使得多线程能更好地为我们服务,提高性能
线程合作
正如在人类世界中,合作是一种非常常见的事情一样,在程序的时间中,合作也是一件非常常见的事情,多个线程共同合作,完成某一个或者某些任务。然而,合作并非说的那么简单,说要合作就能合作,在一般的情况下,我们在合作的过程中会涉及到非常多而且必要的交流,通过交流,我们可以协调合作的过程,比如说,A在生产物品,然后B负责运输A所产生的物品,那么在这样的一个合作的过程中,由于A和B可能会出现一些速度上的差异问题,毕竟任务的难度不一样嘛,所以就会有一些有趣的处理方式了。
处理方式一 轮询机制
处理的方式之一是称之为轮询的方式,具体的过程如下,当A的速度跟不上B的时候,B就每隔一定的时间,比如1s,就跑过来问一下A,物品生产出来了没有啊;反过来,当B的速度更不上A的时候,A就会每个一定时间过来询问B,处理完了没有啊
从上面的操作过程可以看出,这种处理的方式是不太合理的,很明显,无论是A询问B还是B询问A,当两次询问的时间间隔太短的时候,需要频繁地进行询问,当间隔过长的时候,又会导致出现东西堆积太多的情况,而且询问的过程中,A或者B都会浪费一定的资源,比如体力等,这些是属于没必要的消耗。
所以,无论是现实生活中还是在程序开发过程中我们不建议采用这种方式
处理方式二 挂起-通知机制
在上面的操作中,我们可以看到,通过轮询的方式来实现合作的效率是相对比较低的,所以,有了另外一种方式,挂起-通知机制。
同样是上面的场景,此时,如果A的速度跟不上B的速度,则B选择停下来休息(挂起),而不是去询问A,当A准备好的时候,A就主动通知B,然后B重新进入工作模式,反过来也是同理。
上面这种方式是比较适合计算机的工作的,采用这种方式,意味着,当A的速度跟不上B的时候,B选择挂起,也就意味着B放弃CPU,并且交给调度程序重新调度,注意对比轮询,在轮询的方式中,B一直在忙等,也就是说,B一直在占用着CPU,尽管B没有任务可以做。采用挂起-通知机制,可以使得CPU的利用效率相对比较高,使得资源的利用率比较高
线程合作的具体实现
上面大致了解了轮询与挂起-通知机制的区别之后,接下来,就上面的例子,我们来通过代码来具体实现,从具体的代码中来体会这两者的区别
轮询
// 任务A,负责产生物品
class TaskA implements Runnable{
private boolean finished;
public TaskA() {
finished = false;
}
public boolean isFinished() {
return finished;
}
@Override
public void run() {
while (true){
finished = false;
System.out.println("generating ... ");
finished = true;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 任务B,负责搬移物品
class TaskB implements Runnable{
private TaskA taskA;
public TaskB(TaskA taskA) {
this.taskA = taskA;
}
@Override
public void run() {
while (true){
// 轮询A的完成任务情况
while (!taskA.isFinished()){
System.out.println("waiting... ");
}
System.out.println("moving ... ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
从上面代码可以看到,任务B在A没有完成的时候,一直处于空等状态,这是比较消耗资源的
接下来我们来看下基于挂起-通知机制的操作
挂起-通知
在Java中,JDK提供了多种方式用于实现该操作,这里我们先采用最简单的方式wait()
、notify()
wait()操作,顾名思义,当一个线程需要某种资源,而该资源目前不可用的时候,线程可以调用wait操作,使自己阻塞起来
nofity()操作,唤醒线程,表示某种需要的资源已经完成
操作这两个方法有一些需要注意的地方
- 通常这两个操作不是在同一个线程中完成,毕竟既然自己阻塞了自己,那么说明自己缺少资源嘛,需要其他人来唤醒自己。
- 此外,切记,在执行这两个方法的时候,需要注意同步操作,原因在于这两个方法相当于操作信号灯,那么肯定不能同一个信号灯在同一时刻有多个人在处理嘛
- 而且,进行同步的锁必须跟对应的方法的所属对象相同,这也比较好理解,既然是同步操作,那么肯定是或者自己的锁,然后才能操作自己的信号灯嘛
class TaskA implements Runnable{
@Override
public void run() {
while (true){
System.out.println("generating ... ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 同步处理
synchronized (this){
notify(); // 通知等待A的对象
}
}
}
}
class TaskB implements Runnable{
private TaskA taskA;
public TaskB(TaskA taskA) {
this.taskA = taskA;
}
@Override
public void run() {
while (true){
// 同步处理,这里要注意的是,需要同步的是A对象
synchronized (taskA){
try {
taskA.wait();// 等待A完成,如果没有获取到,则会挂起当前线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("moving ... ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
可以看到,这里的处理方式跟轮询是不同的,当没有资源A的时候,B会被挂起,而不是在忙等,关于wait和notify的一些细节问题,我们将在以后的小节中学习到
总结
本小节我们主要学习了线程合作的概念,以及两种通用的合作方式,轮询和挂起-通知机制,其中,挂起-通知机制是一种比较好的处理方式,我们在以后的学习中也会碰到这种模式,在典型的生产者消费者模式中,我们会进行更详细的学习。