第5章 多线程教学设计Word文档格式.docx
《第5章 多线程教学设计Word文档格式.docx》由会员分享,可在线阅读,更多相关《第5章 多线程教学设计Word文档格式.docx(20页珍藏版)》请在冰点文库上搜索。
教
学
过
程
第一课时
(线程概念、线程的创建)
线程概念
✧进程
在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。
目前大部分计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序,最常见的有Windows、Linux、Unix等。
✧线程
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条线索,被称为线程。
✧单线程和多线程的区别
在前面章节所接触过的程序中,代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。
如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。
多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行,如下图所示。
线程的创建
✧两种创建线程的方式
在Java中,线程的创建方式有两种,具体如下:
●继承Thread类,覆写Thread类的run()方法,示例代码如下:
classMyThreadextendsThread{
publicvoidrun(){
while(true){//通过死循环语句打印输出
System.out.println("
MyThread类的run()方法在运行"
);
}
}
}
●实现Runnable接口,示例代码如下:
classMyThreadimplementsRunnable{
//线程的代码段,当调用start()方法时,线程从此处开始执行
publicvoidrun(){
while(true){
✧两种实现多线程方式比较
实现Runnable接口相对于继承Thread类来说,有如下显著的好处:
1、适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
2、可以避免由于Java的单继承带来的局限性。
在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么就只能采用实现Runnable接口的方式。
事实上,大部分的多线程应用都会采用第二种方式,即实现Runnable接口。
第二课时
(线程的生命周期、状态转换)
线程的生命周期
线程整个生命周期可以分为五个阶段,具体如下:
●新建状态(New)
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其它Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
●就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态(也称可运行状态)。
处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得CPU的使用权开始运行,还需要等待系统的调度。
●运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。
当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其它线程获得执行的机会。
需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
●阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。
线程进入阻塞状态后,就不能进入排队队列。
只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
●死亡状态(Terminated)
线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。
一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其它状态。
线程的状态转换
线程的不同状态表明了线程当前正在进行的活动。
在程序中,通过一些操作,可以使线程在不同状态之间转换,如下图所示。
线程由运行状态转成阻塞状态和从阻塞状态转成就绪状态的情形如下:
●当线程试图获取某个对象的同步锁时,如果该锁被其它线程所持有,
则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其它线程所持有的锁。
●当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如
果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
●当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
●当线程调用了Thread的sleep(longmillis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
●当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
第三课时
(线程的调度)
线程的调度
✧概念
程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权。
Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
✧线程调度的模型
线程调度有两种模型,分别是分时调度模型和抢占式调度模型:
●分时调度模型
所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。
●抢占式调度模型
抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其它线程获取CPU使用权。
✧线程的优先级
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。
优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。
线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
除此职位,还可以使用Thread类中提供的三个静态常量表示线程的优先级,具体如下:
✧线程休眠
如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(longmillis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。
当前线程调用sleep(longmillis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其它的线程就可以得到执行的机会了。
需要注意的是,sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其它线程休眠。
当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
✧线程让步
线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。
当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
✧线程插队
在Thread类中提供了一个join()方法来实现这个“功能”。
当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
第四课时
(多线程同步、多线程通信)
多线程同步
✧线程安全问题
模拟窗口售票程序,假如共有10张票出售,并在售票的代码中每次售票时线程休眠10毫秒,如下所示。
publicclassExample11{
publicstaticvoidmain(String[]args){
//创建Ticket1对象
SaleThreadsaleThread=newSaleThread();
//创建并开启四个线程
newThread(saleThread,"
线程一"
).start();
线程二"
线程三"
线程四"
//定义Ticket1类实现Runnable接口
classSaleThreadimplementsRunnable{
privateinttickets=10;
//10张票
while(tickets>
0){
try{
Thread.sleep(10);
//经过此处的线程休眠10毫秒
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"
---卖出的票"
+tickets--);
运行结果如下图所示。
从上图可以看出,售出的票出现了0、-1、-2这样的票号,这种现象是不应该出现的,因此,上面的多线程程序存在安全问题。
✧同步代码块
线程安全问题其实就是由多个线程同时处理共享资源所导致的。
要想解决上述线程安全问题,必须保证用于处理共享资源的代码在任何时刻只能有一个线程访问。
为了实现这种限制,Java中提供了同步机制。
当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块,其语法格式如下:
synchronized(lock){
操作共享资源代码块
上面的代码中,lock是一个锁对象,它是同步代码块的关键。
当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。
当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。
循环往复,直到共享资源被处理完为止。
接下来将例程中售票的代码放到synchronized区域中,示例代码如下:
//定义Ticket1类继承Runnable接口
classTicket1implementsRunnable{
//定义变量tickets,并赋值10
//定义任意一个对象,用作同步代码块的锁
Objectlock=newObject();
synchronized(lock){//定义同步代码块
try{
Thread.sleep(10);
//经过的线程休眠10毫秒
}catch(InterruptedExceptione){
e.printStackTrace();
}
if(tickets>
System.out.println(Thread.currentThread().getName()
+"
+tickets--);
}else{//如果tickets小于0,跳出循环
break;
publicclassExample12{
Ticket1ticket=newTicket1();
//创建Ticket1对象
//创建并开启四个线程
newThread(ticket,"
✧同步方法
了解到同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。
在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized返回值类型方法名([参数1,……]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其它线程都会发生阻塞,直到当前线程访问完毕后,其它线程才有机会执行方法。
使用同步方法对案例进行修改,如下所示。
saleTicket();
//调用售票方法
if(tickets<
=0){
break;
//定义一个同步方法saleTicket()
privatesynchronizedvoidsaleTicket(){
if(tickets>
publicclassExample13{
newThread(ticket,"
运行结果如图所示。
✧死锁问题
在多线程中,当两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
多线程通信
✧问题引出
模拟这样的一种应用场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责向存储空间中存入数据,另一个线程负责则取出数据。
为了实现上述场景,首先定义一个类,在类中使用一个数组来表示存储空间,并提供数据的存取方法,代码实现如下所示:
classStorage{
//数据存储数组
privateint[]cells=newint[10];
//inPos表示存入时数组下标,outPos表示取出时数组下标
privateintinPos,outPos;
//定义一个put()方法向数组中存入数据
publicvoidput(intnum){
cells[inPos]=num;
System.out.println("
在cells["
+inPos+
"
]中放入数据---"
+cells[inPos]);
inPos++;
//存完元素让位置加1
if(inPos==cells.length)
inPos=0;
//当inPos为数组长度时,将其置为0
//定义一个get()方法从数组中取出数据
publicvoidget(){
intdata=cells[outPos];
System.out.println("
从celss["
+outPos+
]中取出数据"
+data);
outPos++;
//取完元素让位置加1
if(outPos==cells.length)
outPos=0;
接下来实现两个线程同时访问上例中的共享数据,这两个线程都需要实现Runnable接口,具体如下所示。
Input.java和Output.java
classInputimplementsRunnable{//输入线程类
privateStoragest;
privateintnum;
//定义一个变量num
Input(Storagest){//通过构造方法接收一个Storage对象
this.st=st;
publicvoidrun(){
while(true){
st.put(num++);
//将num存入数组,每次存入后num自增
classOutputimplementsRunnable{//输出线程类
Output(Storagest){//通过构造方法接收一个Storage对象
publicvoidrun(){
st.get();
//循环取出元素
编写测试程序,用于开启两个线程,具体如下所示:
publicclassExample17{
//创建数据存储类对象
Storagest=newStorage();
//创建Input对象传入Storage对象
Inputinput=newInput(st);
//创建Output对象传入Storage对象
Outputoutput=newOutput(st);
//开启新线程
newThread(input).start();
//开启新线程
newThread(output).start();
从运行结果可以看到,在取出数字12后,紧接着取出的是23,之所以出现这种现象是因为在Input线程存入数字13时,Output线程并没有及时取出数据,Input线程一直在持续地存入数据,直到将数组放满,又从数组的第一位置开始存入21、22、23…,当Output线程再次取数据时,取出的不再是13而是23。
✧问题如何解决
如果想解决上述问题,就需要控制多个线程按照一定的顺序轮流执行,此时需要让线程间进行通信。
在Object类中提供了wait()、notify()、notifyAll()方法用于解决线程间的通信问题。
接下来通过使用wait()和notify()方法,对例程进行改写来实现线程间的通信。
//inPos存入时数组下标,outPos取出时数组下标
privateintcount;
//存入或者取出数据的数量
publicsynchronizedvoidput(intnum){
try{
//如果放入数据等于cells的长度,此线程等待
while(count==cells.length){
this.wait();
cells[inPos]=num;
//向数组中放入数据
inPos++;
//当在cells[9]放完数据后再从cells[0]开始
if(inPos==cells.length)
inPos=0;
count++;
//每放一个数据count加1
this.notify();
}catch(Exceptione){
e.printStackTrace();
publicsynchronizedvoidget(){
while(count==0){//如果count为0,此线程等待
intdata=cells[outPos];
//从数组中取出数据
从cells["
+outPos+
"
cells[outPos]=0;
//取出后,当前位置的数据置0
outPos++;
//当从cells[9]取完数据后再从cells