先看一个问题:

有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次……如此往返执行50次。

看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首先肯定要有两个线程,然后每个线程中肯定有个50次的循环,因为每个线程都要往返执行任务50次,主线程的任务是执行5次,子线程的任务是执行10次。线程间通信技术主要用到 wait() 方法和 notify() 方法。wait() 方法会导致当前线程等待,并释放所持有的锁,notify() 方法表示唤醒在此对象监视器上等待的单个线程。下面来一步步完成这道线程间通信问题。

首先不考虑主线程和子线程之间的通信,先把各个线程所要执行的任务写好:

public class TraditionalThreadCommunication {

	public static void main(String[] args) {
//开启一个子线程
new Thread(new Runnable() { @Override
public void run() {
for(int i = 1; i <= 50; i ++) { synchronized (TraditionalThreadCommunication.class) {
//子线程任务:执行10次
for(int j = 1;j <= 10; j ++) {
System.out.println("sub thread sequence of " + j + ", loop of " + i);
}
}
} }
}).start(); //main方法即主线程
for(int i = 1; i <= 50; i ++) { synchronized (TraditionalThreadCommunication.class) {
//主线程任务:执行5次
for(int j = 1;j <= 5; j ++) {
System.out.println("main thread sequence of " + j + ", loop of " + i);
}
}
}
}
}

如上,两个线程各有50次大循环,执行50次任务,子线程的任务是执行10次,主线程的任务是执行5次。为了保证两个线程间的同步问题,所以用了 synchronized 同步代码块,并使用了相同的锁:类的字节码对象。这样可以保证线程安全。但是这种设计不太好,就像我在上一节的死锁中写的一样,我们可以把线程任务放到一个类中,这种设计的模式更加结构化,而且把不同的线程任务放到同一个类中会很容易解决同步问题,因为在一个类中很容易使用同一把锁。所以把上面的程序修改一下:

public class TraditionalThreadCommunication {

	public static void main(String[] args) {
Business bussiness = new Business(); //new一个线程任务处理类
//开启一个子线程
new Thread(new Runnable() { @Override
public void run() {
for(int i = 1; i <= 50; i ++) {
bussiness.sub(i);
} }
}).start(); //main方法即主线程
for(int i = 1; i <= 50; i ++) {
bussiness.main(i);
}
} }
//要用到的共同数据(包括同步锁)或共同的若干个方法应该归在同一个类身上,这种设计正好体现了高类聚和程序的健壮性。
class Business { public synchronized void sub(int i) { for(int j = 1;j <= 10; j ++) {
System.out.println("sub thread sequence of " + j + ", loop of " + i);
}
} public synchronized void main(int i) { for(int j = 1;j <= 5; j ++) {
System.out.println("main thread sequence of " + j + ", loop of " + i);
}
}

经过这样修改后,程序结构更加清晰了,也更加健壮了,只要在两个线程任务方法上加上 synchronized 关键字即可,用的都是 this 这把锁。但是现在两个线程之间还没有通信,执行的结果是主线程循环执行任务50次,然后子线程再循环执行任务50次,原因很简单,因为有 synchronized 同步。

下面继续完善程序,让两个线程之间完成题目中所描述的那样通信:

public class TraditionalThreadCommunication {

	public static void main(String[] args) {
Business bussiness = new Business(); //new一个线程任务处理类
//开启一个子线程
new Thread(new Runnable() { @Override
public void run() {
for(int i = 1; i <= 50; i ++) {
bussiness.sub(i);
} }
}).start(); //main方法即主线程
for(int i = 1; i <= 50; i ++) {
bussiness.main(i);
}
} }
//要用到共同数据(包括同步锁)或共同的若干个方法应该归在同一个类身上,这种设计正好体现了高雷剧和程序的健壮性。
class Business {
private boolean bShouldSub = true; public synchronized void sub(int i) {
while(!bShouldSub) { //如果不轮到自己执行,就睡
try {
this.wait(); //调用wait()方法的对象必须和synchronized锁对象一致,这里synchronized在方法上,所以用this
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int j = 1;j <= 10; j ++) {
System.out.println("sub thread sequence of " + j + ", loop of " + i);
}
bShouldSub = false; //改变标记
this.notify(); //唤醒正在等待的主线程
} public synchronized void main(int i) {
while(bShouldSub) { //如果不轮到自己执行,就睡
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int j = 1;j <= 5; j ++) {
System.out.println("main thread sequence of " + j + ", loop of " + i);
}
bShouldSub = true; //改变标记
this.notify(); //唤醒正在等待的子线程
}
}

首先,先不说具体的程序实现,就从结构上来看,已经体会到了这种设计的好处了:主函数里不用修改任何东西,关于线程间同步和线程间通信的逻辑全都在 Business 类中,主函数中的不同线程只需要调用放在该类中对应的任务即可。体现了高类聚的好处。

  

再看一下具体的代码,首先定义一个 boolean 型变量来标识哪个线程该执行,当不是子线程执行的时候,它就睡,那么很自然主线程就执行了,执行完了,修改了 bShouldSub 并唤醒了子线程,子线程这时候再判断一下 while 不满足了,就不睡了,就执行子线程任务,同样地,刚刚主线程修改了 bShouldSub 后,第二次循环来执行主线程任务的时候,判断 while 满足就睡了,等待子线程来唤醒。这样逻辑就很清楚了,主线程和子线程你一下我一下轮流执行各自的任务,这种节奏共循环50次。

  

另外有个小小的说明:这里其实用 if 来判断也是可以的,但是为什么要用 while 呢?因为有时候线程会假醒(就好像人的梦游,明明正在睡,结果站起来了),如果用的是if的话,那么它假醒了后,就不会再返回去判断if了,那它就很自然的往下执行任务,好了,另一个线程正在执行呢,啪叽一下就与另一个线程之间相互影响了。但是如果是while的话就不一样了,就算线程假醒了,它还会判断一下 while 的,但是此时另一个线程在执行啊,bShouldSub 并没有被修改,所以还是进到 while 里了,又被睡了~所以很安全,不会影响另一个线程!官方 JDK 文档中也是这么干的。

线程间通信就总结到这吧~若有错误,欢迎指正,我们一起进步。

Java并发基础05. 传统线程同步通信技术的更多相关文章

  1. Java并发基础02. 传统线程技术中的定时器技术

    传统线程技术中有个定时器,定时器的类是Timer,我们使用定时器的目的就是给它安排任务,让它在指定的时间完成任务.所以先来看一下Timer类中的方法(主要看常用的TimerTask()方法): 前面两 ...

  2. Java并发基础03. 传统线程互斥技术—synchronized

    在多个线程同时操作相同资源的时候,就会遇到并发的问题,如银行转账啊.售票系统啊等.为了避免这些问题的出现,我们可以使用synchronized关键字来解决,下面针对synchronized常见的用法做 ...

  3. Java并发基础01. 传统线程技术中创建线程的两种方式

    传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...

  4. Java多线程与并发库高级应用-传统线程同步通信技术

    面试题: 子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着又 主线程循环100次,如此循环50次,请写出程序 /** * 子线程循环10次,接着主线程循环100次,接着又回到 ...

  5. 【Java多线程与并发库】4.传统线程同步通信技术

    我们先通过一道面试题来了解传统的线程同步通信. 题目:子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次,请写出程序. 我没有看答案, ...

  6. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  7. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  8. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  9. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

随机推荐

  1. 微信APP生命周期、页面生命周期

    目录 小程序的启动流程 app生命周期 页面的生命周期 页面的生命周期(图) 小程序的启动流程 我们画一个图来表示一下,整个小程序的启动流程,我们就知道了: app生命周期 执行App()函数也就是注 ...

  2. idea创建简单web项目分析Servlet的请求转发与重定向的区别

     注:如需转载,请附上原文链接,如有建议或意见,欢迎批评指正! 需求说明: // index.jsp页面 1 <% 2 String basePath = request.getScheme() ...

  3. MyBatis 源码分析-项目总览

    MyBatis 源码分析-项目总览 1.概述 本文主要大致介绍一下MyBatis的项目结构.引用参考资料<MyBatis技术内幕> 此外,https://mybatis.org/mybat ...

  4. HDFS NameNode详解

    1. namenode介绍 namenode管理文件系统的命名空间.它维护着文件系统树及整棵树内所有的文件和目录.这些信息以两个文件形式永久保存在本地磁盘上:命名空间镜像文件fsimage和编辑日志文 ...

  5. 如何在PHP7中扩展mysql,先安装php7.2。后安装mysql

    相对与PHP5,PHP7的最大变化之一是移除了mysql扩展,推荐使用mysqli或者pdo_mysql,实际上在PHP5.5开始,PHP就着手开始准备弃用mysql扩展,如果你使用mysql扩展,可 ...

  6. 阿里AI芯片:12nm工艺、709平方毫米大核心

    含光出自<列子·汤问>篇有“上古三剑”一章,寓意含而不露,光而不耀,象征含光 800 无形却强劲的算力. 含光 800 是一款 AI 芯片,偏重推理.据介绍,1 颗含光 800 的算力相当 ...

  7. 前端BOM和DOM

      前端基础之BOM和DOM   前戏 到目前为止,我们已经学过了JavaScript的一些简单的语法.但是这些简单的语法,并没有和浏览器有任何交互. 也就是我们还不能制作一些我们经常看到的网页的一些 ...

  8. SpringBoot——Cache使用原理及Redis整合

    前言及核心概念介绍 前言 本篇主要介绍SpringBoot2.x 中 Cahe 的原理及几个主要注解,以及整合 Redis 作为缓存的步骤 核心概念 先来看看核心接口的作用及关系图: CachingP ...

  9. js运算【按位非】~ (index = ~~this.userIndex)(~~ 双破折号 如果是数字返回数字,如果不是数字 返回0)

    index = ~~this.userIndex ~~ 双破折号 如果是数字返回数字,如果不是数字 返回0 这个运算符有点意思:按位非[~] 先来几个例子: ~undefined: -1 ~false ...

  10. apply 和 call 方法详解【转载】

    本文转载至:http://blog.csdn.net/business122/article/details/8000676 我在一开始看到javascript的函数apply和call时,非常的模糊 ...