1、项目9 利用多线程技术实现定时响铃古凌岚 张婵 罗佳人民邮电出版社JavaJava系统化项目开发教程系统化项目开发教程知识要点n 线程的生命周期n 线程的创建 n 线程并发控制n 线程通信引子 n 为什么需要引入线程?u日常生活中,我们人在同一时间可以做许多事,如在进餐的同时,还可以倾听朋友叙述的故事uJava程序中为了模拟这种功能,引入了线程机制。简单点说,当一个程序能够同时完成多件事情时,就是所谓的多线程程序u多线程应用较广泛9.1实战任务十一:实现闹钟启动的计时功能n 问题分析u设定闹钟涉及两个时间点,一是闹钟时间,二是设置闹钟时的系统时间,当这两个时间点重叠时,铃声音乐将被播放,即闹钟
2、响了u闹钟设定后,需要不断地计算两个时间点间的差值,同时还要求能够使用闹钟的其它功能,如上传铃声文件等u多线程技术,可实现以上多个事务处理的互不影响,各自独立执行的效果9.1实战任务十一:实现闹钟启动的计时功能n 进程和线程u程序是指具有完整功能的一段静态代码u进程是指多任务操作系统中,每个程序的一次动态执行。其对应着代码加载、执行和直至执行完毕的一个完整过程u处理器会为每次加载的进程分配一个独立的内存空间,若同一段代码被加载多次,则会被分配到不同的内存空间加以执行u线程是CPU的最小执行单位,是进程内部的执行线索,但它与进程同样也有产生、发展和消亡三个阶段9.1实战任务十一:实现闹钟启动的计
3、时功能u进程和线程的关系9.1实战任务十一:实现闹钟启动的计时功能u多线程示例运行结果9.1实战任务十一:实现闹钟启动的计时功能u上述程序执行的过程如下,可了解线程的整个生命周期(1)执行main方法,创建一个aloneThread类对象;(2)调用aloneThread类中aloneThread构造方法;(3)创建线程myThread对象,调用start方法,启动该线程;(4)执行线程体run方法;(5)执行FOR循环;(6)在屏幕上打印出数字0-5,每打印出一个数字,调用sleep方法,让线程进入休眠状态(暂停)1000毫秒;(7)在循环过程中,若中断程序,则显示出错误信息;(8)循环执行
4、完成,程序结束。9.1实战任务十一:实现闹钟启动的计时功能n 线程五个状态9.1实战任务十一:实现闹钟启动的计时功能n 创建线程u方法一是创建Thread类的子类u方法二是利用Runnable接口u方法三是利用Callable接口9.1实战任务十一:实现闹钟启动的计时功能n Thread类u通过创建该类的子类,来创建线程uThread类的常用方法9.1实战任务十一:实现闹钟启动的计时功能n 方法一示例9.1实战任务十一:实现闹钟启动的计时功能n 利用Runnable接口uRunnable接口只有一方法public void run()应用示例如右侧代码9.1实战任务十一:实现闹钟启动的计时功能
5、n 线程并发控制u一个进程中的多个线程要共用当前进程的资源,如内存空间、变量、I/O设备等u如何有序高效地利用资源,是多线程应用的关键问题9.1实战任务十一:实现闹钟启动的计时功能n 相关术语u 并行:多个CPU实例或者多台机器同时执行一段处理逻辑,是真正的同时u 并发:通过CPU调度算法,让用户感觉是同时执行,但实际并非真的同时u 线程安全:指在并发情况下,某段代码经过多线程调用,线程的调用顺序不会影响这段代码的执行结果u 互斥:当多个线程需要访问同一资源时,要求在一个时间段内只能允许一个线程来操作共享资源(临界区)。这里线程之间不需要知道对方的存在,执行顺序是乱序u 同步:线程间需要通过交
6、替执行,访问公共资源,来共同完成一个任务u 线程安全:指要控制多个线程对某个资源的有序访问或修改9.1实战任务十一:实现闹钟启动的计时功能n 互斥性、可见性和顺序性uJava引入了synchronized 关键字,实现了互斥性、可见性和顺序性,以保证多个线程有序访问进程资源9.1实战任务十一:实现闹钟启动的计时功能n 未使用互斥锁的存取款示例9.1实战任务十一:实现闹钟启动的计时功能9.1实战任务十一:实现闹钟启动的计时功能n 从运行结果可知,有时存取款的顺序有误(如第二组结果)9.1实战任务十一:实现闹钟启动的计时功能n 在方法上加了互斥锁的存取款示例9.1实战任务十一:实现闹钟启动的计时功
7、能9.1实战任务十一:实现闹钟启动的计时功能n 从这次运行结果可知,存取款线程是有序进行的9.1实战任务十一:实现闹钟启动的计时功能n 利用synchronized实现互斥性的原理uJava中的每个对象都包含了一把锁,它自动成为对象的一部分(不必为此写任何特殊的代码)u调用该对象的任何synchronized方法时,对象就会被锁定,该对象的其他synchronized方法此时也都不可被调用,除非第一个方法完成了自己的工作,并解除锁定9.1实战任务十一:实现闹钟启动的计时功能n 利用synchronized实现线程同步u使用synchronized修饰方法,该方法称为同步方法synchroniz
8、ed修饰成员方法时,则一个线程要执行该方法,必须取得该方法所在的对象的锁,即某个synchronized成员方法被调用时,其它synchronized成员方法不可被调用;synchronized修饰类方法时,则一个线程要执行该方法,必须获得该方法所在的类的类锁,即某个synchronized类方法被调用时,其它synchronized类方法同时也被锁住synchronized修饰一个代码块,则可使得当不同块所需的锁不冲突时,则不必对整个对象加锁。采用对代码块加锁,可以减小锁的粒度9.1实战任务十一:实现闹钟启动的计时功能代码块同步的形式为vsynchronized(obj)/obj表示对象,为
9、共享资源/code9.1实战任务十一:实现闹钟启动的计时功能n 在代码块上加互斥锁的使用示例9.1实战任务十一:实现闹钟启动的计时功能n 从运行结果可知,两个线程的执行是有序的9.1实战任务十一:实现闹钟启动的计时功能n 异步多线程的同步执行u有时主线程和子线程是异步启动的,但又需要以同步方式执行u实现方法join方法isAlive方法u示例先来看一个程序代码9.1实战任务十一:实现闹钟启动的计时功能u上述代码执行时,无法显示出a+的最后结果,为什么?ujoin方法使用join方法,修改上述代码中黑色字体部分,可以解决问题9.1实战任务十一:实现闹钟启动的计时功能ualive方法alive应用
10、示例运行结果9.1实战任务十一:实现闹钟启动的计时功能9.1实战任务十一:实现闹钟启动的计时功能n 死锁现象u两个或多个线程分别拥有不同的资源,而同时又需要对方释放资源才能继续运行u如果锁使用不当,就会出现死锁现象9.1实战任务十一:实现闹钟启动的计时功能n 死锁的产生需同时满足四个条件(以过独木桥为例)u互斥条件:一个资源每次只能被一个进程使用。独木桥每次只能通过一个人。u请求与保持条件:一个进程因请求资源而阻塞时,并对已获得的资源保持不放。乙不退出桥面,甲也不退出桥面。u不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。甲不能强制乙退出桥面,乙也不能强制甲退出桥面。u循环等待条件
11、:若干进程之间形成一种头尾相接的循环等待资源关系。如果乙不退出桥面,甲不能通过,甲不退出桥面,乙不能通过。9.1实战任务十一:实现闹钟启动的计时功能n 避免死锁现象的解决方法u需要加多个锁时,按照顺序来加;u设置锁的超时时限;u创建线程和锁的关系图,通过对其进行检测,防止死锁。9.1实战任务十一:实现闹钟启动的计时功能n 线程优先级uJava支持为每个线程设置优先级u优先级高的线程在排队执行时,会有更多机会获得CPU资源去运行uThread类规定了三个优先级MAX_PRIORITY最高优先级NORM_PRIORITY普通优先级,也是默认优先级MIN_PRIORITY最低优先级9.1实战任务十一
12、:实现闹钟启动的计时功能uThread类的setPriority方法可用于设置线程的优先级public final void setPriority(int newPriority)/setPriority方法声明setPriority方法应用示例9.1实战任务十一:实现闹钟启动的计时功能 运行结果9.1实战任务十一:实现闹钟启动的计时功能n 需要说明的是u虽然设置了线程优先级,仍会无法确保该线程一定先执行,它有很大的随机性。因为线程的执行,是抢占资源后才能执行的操作,最多是给于线程优先级较高的多一点抢占资源的机会而已,能不能抢到却是不一定的9.1实战任务十一:实现闹钟启动的计时功能n 线程通
13、信机制u实际应用中,需要多个任务彼此间可以协作,一起解决某个问题,例如:制作木桌,其工序及顺序为:购买木材、制作桌子各部件、上油漆和组装,需按照顺序完成所有工序才算是完工u为了实现线程间协作(线程通信),需要解决两个问题线程间共享资源的互斥是线程执行状态的传递9.1实战任务十一:实现闹钟启动的计时功能n 对于线程间共享资源的互斥的另一个途径uJava可以让当前线程挂起,直到某些外部条件发生变化,表示可以让当前线程继续u具体的方法是,通过Object的wait、notify和notifyAll方法来实现的,这些均为final方法)9.1实战任务十一:实现闹钟启动的计时功能n wait方法u作用是
14、告诉当前线程放弃对当前对象监视器的控制权并睡眠,直到另一个线程调用notify方法upublic final void wait()throws InterruptedException/方法声明n notify方法u作用是唤醒正在等候当前对象监听器的单个线程。若有多个线程在等待,则任选其中一个。但只有拥有对象监视器的线程才能使用该方法upublic final void notify()/方法声明9.1实战任务十一:实现闹钟启动的计时功能n 线程通信的实现u先看一个例子:苹果丰收了,果农会将苹果摘下来,分等级出售。摘苹果的时候,果农甲将树上的苹果摘下放入筐中,树下的果农乙会从这个筐中取苹果,
15、依质量好坏分类放入其他筐中。甲不停摘下苹果放入筐中,乙不停地从筐中取出苹果。如果筐中苹果已取完,则乙必须等待直到甲又摘了苹果放入筐中;如果筐已满,而乙还未拿走一个的话,甲也必须等待直到乙开始取苹果9.1实战任务十一:实现闹钟启动的计时功能u两个线程间的协作(通信)类似于摘苹果,两个线程间的协作(通信),产生数据(苹果)的线程被称为生产者,而使用数据的线程为消费者以摘苹果为例,实现生产者和消费者的示例,完整代码参见 教材9.1实战任务十一:实现闹钟启动的计时功能n 任务实施u利用多线程机制,实现闹钟工具软件中,定时判断是否到达定时时间的功能u创建内部类TimeListen线程,用于监听,即每隔1
16、秒获取一次剩余时间,且一旦到闹铃时间,则播放铃声(在下一任务中实现)u在启动闹钟设置时,启动TimeListen线程u完整代码参见教材9.1实战任务十一:实现闹钟启动的计时功能n 同步练习u实现闹钟工具软件中当前时间的动态显示。n 同步练习u音乐播放器项目MusicPlayerProj中,实现音乐播放进度条的动态显示。9.2实战任务十二:实现铃声播放功能n 问题分析u闹钟工具软件还有几个任务需要同时执行需要实时地显示当前系统时间一旦到达设置的闹钟时间,还需要播放所选定的铃声音乐。铃声播放线程,与实战任务十一中的闹钟定时判断是否到达设定时间线程,有逻辑关系,但此间闹钟工具中其它功能应可以使用,因
17、此,也需要定义为线程9.2实战任务十二:实现铃声播放功能n 音乐播放uJava提供了音乐播放API,支持wav格式文件ujavax.sound.sampled包提供了AudioInputStream、AudioFormat、AudioSystem、DataLine和SourceDataLine,用于实现音乐文件的读取、格式定义,以及输出到混频器等操作9.2实战任务十二:实现铃声播放功能n 未使用线程音乐播放示例9.2实战任务十二:实现铃声播放功能9.2实战任务十二:实现铃声播放功能9.2实战任务十二:实现铃声播放功能u音乐播放的流程(1)调用AudioSystem类的getAudioInput
18、Stream静态方法,创建音频输入流类AudioInputStream的对象audioInputstream;(2)调用audioInputstream对象的getFormat方法,创建音频格式对象format;(3)实例化嵌套类DataLine.info,根据指定格式format,构造信息对象;(4)调用AudioSystem类的getLine静态方法,获得信息对象的源数据行SourceDataLine类对象dataline;(5)调用dataline对象的start方法,启动流的读写操作;(6)调用audioInputstream对象的read方法,从音乐文件中读取内容到字节数组bufby
19、tes中,如果读取值为-1,则表示文件读取完毕;(7)调用dataline对象的write方法,每次将字节数组bufbytes中的readbytes个字节写入到混频器中,即播放音乐。9.2实战任务十二:实现铃声播放功能n 同步练习u音乐播放器项目MusicPlayerProj中,能够选择音乐,并播放音乐9.2实战任务十二:实现铃声播放功能n 利用线程实现动画u动画实现的基本原理Java的图形界面,每个组件都是由paint/paintComponent方法绘制出来的,通过定时重新绘制组件,就可以产生动画效果,定时触发绘制是由线程来实现的,绘制动作本身则是通过重写paint/paintCompon
20、ent方法完成。9.2实战任务十二:实现铃声播放功能n 时钟动画的实现u实现效果u源码参见教材9.2实战任务十二:实现铃声播放功能u时钟实现的关键依据三角函数来计算时、分、秒针的位置,如指针的x、y坐标,分别来自于sin和cos公式,角度的计算则是根据圆的弧度与指针位置的变化,如对于时针,将圆形分割是为12份,即每小时的弧度2*Math.PI/12,由于每天为24小时,而时钟圆上仅有12个刻度,需要进行this.hour%12运算,同时结合分钟的变化this.minute/60.0,最终得到时针所在的准确角度9.2实战任务十二:实现铃声播放功能重写paintComponent方法,用于绘制时钟
21、定义curTime方法,用于设置新一帧画面的时、分、秒值,并调用repain方法,使得paintComponent被重新调用,达到刷新时钟画面的目的9.2实战任务十二:实现铃声播放功能n 同步练习u设计并实现一个具有动画效果的生日贺卡,包括音乐、动画和文字内容9.2实战任务十二:实现铃声播放功能n 任务实施u将音乐播放作成一个线程,方便控制u当点击“试听”时,播放指定音乐,当点击“停止”时,判断音乐播放线程是否仍存在,存在则关闭该线程u当闹钟设置达到闹铃时间时,播放指定音乐u完整代码参见教材知识延伸n Java中终止正在运行的线程,有以下三种方法:u(1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。u(2)使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期方法。u(3)使用interrupt方法中断线程9.2实战任务十二:实现铃声播放功能n 同步练习u音乐播放器项目MusicPlayerProj中,改进音乐播放功能,使得音乐播放过程中,可以暂停或停止音乐