1、(时间:3次课,6学时)n教学提示:教学提示:计算机世界要想真正地反映现实世界,必须计算机世界要想真正地反映现实世界,必须解决事情的同步问题,即解决程序实现多线程的问题。解决事情的同步问题,即解决程序实现多线程的问题。n因此可编写有几条执行路径的程序,使得程序能够同时因此可编写有几条执行路径的程序,使得程序能够同时执行多个任务,借此实现多线程运行。执行多个任务,借此实现多线程运行。Java语言的一大语言的一大特点就是内置对多线程的支持。特点就是内置对多线程的支持。n本章主要介绍:本章主要介绍:Java中的线程作用机制中的线程作用机制、线程的实现方、线程的实现方法、线程的控制和线程的同步与死锁法
2、、线程的控制和线程的同步与死锁。n6.1 线程简介线程简介n6.2 线程的实现方法线程的实现方法n6.3 线程的控制线程的控制n6.4 Java的多线程实例的多线程实例n6.5 线程的同步与死锁线程的同步与死锁n6.6 ThreadLocal问题问题n6.7 课后练习课后练习 n6.1.1 程序、进程和线程程序、进程和线程n6.1.2 线程的生命周期线程的生命周期 n6.1.3 线程的优先级及其调度线程的优先级及其调度6.1.4 线程组线程组 n对于许多编程人员来说,线程并不是那么的陌生。但是在Java中,线程的作用机制又是如何工作的呢?本节将重点介绍Java中的线程作用机制。n程序是由若干条
3、语句组成的语句序列,是一段静态代码。n进程是程序的一次动态执行过程。n需要特别指出的是,进程不仅包括程序代码,还包括系统资源。即一个进程既包括其所要执行的指令,也包括执行指令所需的任何系统资源,如CPU、内存空间等。不同进程所占用的系统资源相对独立。n线程又是一个抽象的概念,它包含了一个计算机执行传统程序时所做的每一件事情。线程是一种在CPU上调度的程序状态,它在某一瞬时看来只是计算过程的一个状态。一个进程中的所有线程共享该进程的状态,它们存储在相同的内存空间中,共享程序的代码和数据。所以当其中一个线程改变了进程的变量时,那么其他线程下次访的将是改变后的变量。n多线程是指同一个应用程序中有多个
4、顺序流同时执行。在一个程序中可以同时运行多个不同的线程来执行不同的任务,各个线程并行地完成各自的任务。浏览器就是一个典型的多线程例子。n每个Java程序都有一个默认的主线程。对于应用程序,主线程是main()方法执行的路径。图6-1说明线程的生命周期及其状态转换。n图6-1 线程的状态转换n从图6-1中可以看出:一个线程从创建到消亡的整个生命周期中,总是处于下面5个状态中的某个状态。n1.新建状态n通过new命令创建一个Thread类或其子类的线程对象时,该线程对象处于新建状态。创建一个新的线程对象可以用下面的语句实现:nThread thread=new Thread();n该语句是最简单的
5、创建线程的语句,但该语句创建的线程是一个空的线程对象,系统还未对这个线程分配任何资源。n2.就绪状态n该状态又可称为可运行状态。处于新建状态的线程可通过调用start()方法启动该线程。Start()方法产生了线程运行需要的系统资源。启动后的线程将进入线程就绪队列排队等待CPU服务,此时线程已经具备了运行的条件,一旦它获得CPU等资源时就可以脱离创建它的主线程而独立运行。n3.运行状态n当处于就绪状态的线程被调度并获得CPU资源时,使进入运行状态。每个线程对象都有一个重要的run()方法,run()方法定义了该线程的操作和功能。当线程对象被调度执行时,它将自动调用其run()方法并从第一条语句
6、开始顺次执行。n4.阻塞状态n又称不可运行状态。当发生下列情况之一时,线程就进入阻塞状态。n(1)等待输入输出操作完成。n(2)线程调用wait()方法等待一个条件变量。n(3)调用了该线程的sleep()休眠方法。n(4)调用了suspend()挂起方法。n5.消亡状态n消亡状态又称死亡状态,当调用run()方法结束后,线程就进入消亡状态,这是线程的正常消亡。另外线程还可能被提前强制性消亡。不管何种情况,处于消亡状态的线程不具有继续运行的能力。n线程被创建之后,每个Java线程的优先级都在Thread.MIN_PRIORITY(常量1)和Thread.MAX_PRIORITY(常量10)的范
7、围之内。每个新建线程的默认优先级都为Thread.NORM_PRIORITY(常量5)。可以用方法int getPriority()来获得线程的优先级,同时也可以用方法 void setPriority(int p)在线程被创建后改变线程的优先级。n一个线程将始终保持运行状态,直到出现下列情况:由于I/O(或其他一些原因)而使该线程阻塞;调用sleep、wait、join 或yield 方法也将阻塞该线程;更高优先级的线程将抢占该线程;时间片的时间期满而退出运行状态或线程执行结束。n【例例6.3】综合使用线程的方法来控制线程的工作举例,程序如下。综合使用线程的方法来控制线程的工作举例,程序如下
8、。n/一个实现一个实现Runnable接口的接口的SimpleRunnable类类nclass SimpleRunnable implements Runnable n protected String message;n protected int iterations;n public SimpleRunnable(String msg,int iter)n message=msg;n iterations=iter;n n public void run()n for(int i=0;iiterations;i+=1)n System.out.println(message);n try
9、 n Thread.sleep(100);n catch(InterruptedException e)n System.out.println(e);n n n nn/ThreadExample类运行这个线程类运行这个线程npublic class ThreadExample n public static void main(String args)n Thread t1,t2;n t1=new Thread(new SimpleRunnable(Thread 1,10);n t2=new Thread(new SimpleRunnable(Thread 2,15);n System.ou
10、t.println(T1 p is:+t1.getPriority();n System.out.println(T2 p is:+t2.getPriority();n t2.setPriority(7);n System.out.println(T2 after set p is:+t2.getPriority();n t2.yield();n System.out.println(T2 after yield p is:+t2.getPriority();n t1.start();n t2.start();n nn运行结果如下:运行结果如下:nT1 p is:5nT2 p is:5nT2
11、after set p is:7nT2 after yield p is:7nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 1nThread 2nThread 2nThread 2nThread 2nThread 2n线程组(Thread Group)允许把一组线程统一管理。例如,可以对一
12、组线程同时调用interrupt()方法,中断这个组中所有线程的运行。创建线程组的构造方法为:nThreadGroup(String groupName);n线程组构造方法的字符串参数用来标识该线程组,并且它必须是独一无二的。线程组对象创建后,可以将各个线程添加到该线程组,在线程构造方法中指定所加入的线程组:nThread(groupName,threadName);n线程组下可以设子线程组,默认创建线程组将成为与当前线程同组的子线程组。当线程组中的某个线程由于一个异常而中止运行时,ThreadGroup类的uncaughtException(Threadt,Throwable e)方法将会打
13、印这个异常的堆栈跟踪记录。n6.2.1 继承继承Thread类类n6.2.2 实现实现Runnable接口接口 n要使用多线程实现程序的流程控制,需要首先创建线程,生成线程实例,然后需要控制线程的调度。线程的创建分两步:定义线程体和创建线程对象。线程体决定线程的功能,它由run()方法实现,系统通过调用线程的run()方法实现线程的具体功能。Java中可通过继承Thread类或实现Runnable接口这两种途径来构造线程的run()方法。n可通过创建Thread类的子类并重写其中的run()方法来定义线程体以实现线程的具体功能,然后创建该子类的对象以创建线程。nThread类在包java.la
14、ng中,它定义了Java程序中一个线程需要拥有的属性和方法。如表6-1和表6-2所示。n表6-1 Thread类的属性n表6-2 Thread类的构造方法和关键方法n另一种实现多线程的方法是实现Runnable接口。Runnable接口只定义了一个方法run()方法,该方法是一个抽象方法,所有实现Runnable接口的类都必须具体实现这个方法,为它提供方法体并定义具体操作。Runnable接口中的 run()方法与Thread 类中的run()方法一样,是被系统自动识别执行的。n通过实现Runnable接口实现多线程的一般步骤如下。n(1)创建实现Runnable接口的类ClassName。它
15、的一般格式为:nclass ClassName implements Runnablenn public void run()n n /编写代码n nn(2)创建Runnable类ClassName的对象。一般格式为:nClassName RunnableObject=new ClassName();n(3)用带有Runnable参数的Thread类构造方法创建线程对象,对象RunnableObject作为构造方法的参数,作为新建线程的目标对象为线程提供 run()方法。如用表 8-2 中列出的构造方法Thread(Runnable target)创建线程对象:nThread ThreadOb
16、ject=new Thread(RunnableObject);nThread 类中除了上述构造方法带有Runnable 参数外,还有下面3个构造方法也带有Runnable参数。nThread(Runnable target,String name)nThread(ThreadGroup group,Runnable target)nThread(ThreadGroup group,Runnable target,String name)n其中Runnable参数是一个实现了Runnable接口的某个类的对象。第2个和第3个方法的第一个参数是指明新创建的线程属于哪一个线程组。n6.3.1 启动
17、线程启动线程n6.3.2 线程休眠线程休眠n6.3.3 中断线程中断线程 n父线程通过创建Thread对象并调用其start方法来启动子线程。start方法使该对象成为一个准备运行的新线程。第一次轮到该线程执行时,JVM就会调用run方法。线程可以处于以下5种状态的任何一种状态:新建、就绪、运行、阻塞和消亡状态。n在Thread类里面,提供了一些控制多线程状态的方法,如表6-3所示。n表6-3 线程的控制方法n通过这些方法,线程的各个状态可以相互转换。转换的情况和Thread类的控制方法对线程的影响参见图6.3。n下面详细讲解这些方法的用法。n(1)start()启动线程n当Thread类的实
18、例对象已经创建了,但没做任何事情时,就需要我们调用start()方法来启动线程。start()方法使Thread对象表示的虚拟CPU开始执行,也就是切换到就绪状态,在这之后,新线程开始执行可运行的run()方法。如果我们不是调用start()方法使线程执行,而是寄希望于直接调用run()方法来执行代码,实际上只是调用了一个名称为run()的方法,而不是一个线程。n1:start();2:stop();3:suspend();4:sleep();5:resume();6:yield();7:interrupt();8:等待处理资源n图6-3 线程的生命周期n(2)stop()停止线程停止线程ns
19、top()方法使线程停止,它激发死亡状态并且给出错误。但是,stop()方法只是Thread类的方法,当我们用Runnable接口创建线程时,我们是不能用stop()方法使线程停止的。n(3)suspend()暂停线程暂停线程nsuspend()方法也只是Thread类的方法,调用这个方法可以使线程处于封锁状态,直至我们调用resume()方法来唤醒它,suspend()方法可以被自己和别的线程调用,通常是被别的线程调用,比如被鼠标单击事件处理程序调用。暂停后,该线程不能做任何事情,甚至没有办法调用resume()方法使自己重新启动,直至有别的线程调用resume()方法使之恢复。n(4)sl
20、eep()暂停线程暂停线程nsleep()方法使控制流程暂停,在给定的段时间内睡眠,所以特别适合需要在一定时间间隔内完成一个动作的线程。睡眠的时间由参数来决定,毫秒级和纳秒级分别对应两个方法:npublic static void sleep(long millis)throws InterruptException;/睡眠millis毫秒npublic static void sleep(long millis,int nanos)throws InterruptException;/睡眠millis毫秒加上nanos纳秒n(5)resume()恢复线程恢复线程n调用resume()方法使线
21、程重新启动。n(6)yield()使线程由运行状态转为就绪状态使线程由运行状态转为就绪状态n如果要编写多个合作线程,则可能浪费CPU时间。n(7)interrupt()唤醒正在睡眠的线程唤醒正在睡眠的线程n如果某个线程因调用了sleep()方法而暂停,可以调用interrupt()方法将之唤醒,后者用异常InterruptException来强制使sleep的调用提前返回。n另外,还有join()方法,调用join()方法后线程暂停工作,并一直等待直到线程停止。n【例例6.5】使用使用start方法启动线程,程序如下。方法启动线程,程序如下。nclass ThreadBody implemen
22、ts Runnablenn int i;n public void run()n n tryn n Thread.currentThread().sleep(100);n System.out.println(Thread.currentThread().getName();n ThreadBody tb=new ThreadBody();n Thread th=new Thread(tb);n th.start();n n catch(InterruptedException e)n n e.printStackTrace();n n nnclass Testnn public static
23、 void main(String args)n n ThreadBody body=new ThreadBody();n Thread newThr=new Thread(body);n newThr.start();n nn运行结果如图6-4所示。n由图可知,这个程序是个相当于死循环的程序,因为它在线程里启动新的线程,这样无休止地启动,最终导致了死循环的结果。n图6-4 运行结果(例6.5)n【例例6.6】使用使用sleep方法使线程休眠,程序如下。方法使线程休眠,程序如下。nclass EvenOdd extends Threadnn private int f,delay;n publ
24、ic EvenOdd(int first,int interval)n n f=first;n delay=interval;n n public void run()n n tryn n for(int i=f;i=100;i+=2)n n System.out.println(Thread.currentThread().getName()+i);n sleep(delay);n n n catch(InterruptedException e)n n e.printStackTrace();n n nnclass Testnn public static void main(String
25、 args)throws InterruptedExceptionn n EvenOdd th1=new EvenOdd(1,20);n EvenOdd th2=new EvenOdd(0,30);n th1.start();n th2.start();n th1.join();n th2.join();n System.out.println(Main thread done);n nn运行结果如图6-5所示。n图6-5 运行结果(例6.6)n中断一个线程意味着在完成其任务以前,停止线程正在进行的工作,即有效地中止当前操作。线程中断后是等待新的任务还是继续进行下一步操作将取决于应用程序。n这
26、里就必须注意的问题提出了一些建议。n(1)不要使用不要使用Thread.stop方法方法n尽管它的确可以中止一个正在运行的线程,由于它的安全问题而遭到了开发人员普遍的反对。这也可能意味着在未来的Java版本中它可能不会出现。n(2)不建议使用不建议使用Thread.interruptnThread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。n【例例6.8】通过通过Thread.interrupt()中断线程举例,程序如下。中断线程举例,程序如下。nclass Example1 extends T
27、hreadnnpublic static void main(String args)throws Exception nnExample1 thread=new Example1();nSystem.out.println(Starting thread.);nthread.start();nthread.sleep(3000);nSystem.out.println(Interrupting thread.);nthread.interrupt();nThread.sleep(3000);nSystem.out.println(Stopping application.);nSystem.
28、exit(0);nnpublic void run()nnwhile(true)nnSystem.out.println(Thread is running.);nlong time=System.currentTimeMillis();/当前系统毫秒级单位的时间,返回长整型的数当前系统毫秒级单位的时间,返回长整型的数nwhile(System.currentTimeMillis()-time 0)n n tryn n Thread.sleep(10);/让线程等待让线程等待10毫秒毫秒 System.out.println(Thread.currentThread().getName()+卖
29、了第卖了第+index+张饭票张饭票);n index-;n n catch(InterruptedException e)n n e.printStackTrace();/打印异常出现的轨迹打印异常出现的轨迹n n n n nn运行结果如图运行结果如图6-9所示。所示。n图6-9 运行结果(例6.10)n6.5.1 线程的同步线程的同步n6.5.2 死锁死锁n6.5.3 线程同步示例线程同步示例n6.5.4 设置线程优先级示例设置线程优先级示例 n在使用线程时,往往会出现意料不到的结果,为了解决这些问题,必须了解Java中线程的死锁和同步的问题。n通常来说,同时运行的线程需要共同数据,在这种
30、情况下,就需要考虑其他线程对当前线程的影响。n在Java中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有个线程访问该对象。关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。n引入互斥锁之后,需要使用wait(),notify()和notifyAll()方法来同步线程的执行,这些方法的使用说明如下。n(1)wait(),notify()和notifyAll()三种方法必须在已经持有锁的情况下执行,只能出现在synchronize
31、d作用的范围内。n(2)wait()方法的作用是释放已持有的锁,进入wait()队列。n(3)notify()的作用是唤醒wait队列中的第一个线程,并将该线程移入互斥锁申请队列。n(4)notifyAll()方法的作用是唤醒wait队列中的所有的线程,并将这些线程移入互斥锁申请队列。nJava中实现互斥锁是通过使用synchronized关键字,它包括两种用法:synchronized方法和synchronized块(也说同步方法和同步块)。n1.synchronized 方法n通过在方法声明中加入synchronized关键字来声明synchronized 方法。使用的格式为:npubli
32、c synchronized void accessVal(int newVal);n2.synchronized 块n通过synchronized关键字来声明synchronized块。语法如下:nsynchronized(syncObject)nn /允许访问控制的代码nn多线程在使用互斥机制实现同步的时候,存在“死锁”的潜在危险。如果多个线程都是处于等待状态而无法被唤醒时,就构成死锁。例如一个线程进入对象ObjA上的监视器,而另一个线程进入对象ObjB上的监视器。如果ObjA中的线程试图调用ObjB上的任何 synchronized 方法,就将发生死锁。此时处于等待状态的多个线程占用系统
33、资源,但又无法运行,因此不会释放自己的资源,由于系统资源有限,程序停止运行。nJava技术既不能发现死锁也不能避免死锁。所以编程时应注意死锁问题,尽量避免。n如何去解决死锁的问题,看起来是一件很头痛的事,因为它涉及CPU的时间片的分配等问题。最有效方法还是避免死锁的发生,一般有以下两种方法:n线程因为某个条件未满足而受阻,不能让其继续占有资源。n如果有多个对象需要互斥访问,应确定线程获得锁的顺序。n【例例6.13】线程同步示例。线程同步示例。npublic class Producer extends Thread n private CubbyHole cubbyhole;n private
34、 int number;n public Producer(CubbyHole c,int number)n cubbyhole=c;n this.number=number;n n public void run()n for(int i=0;i 10;i+)n cubbyhole.put(i);n System.out.println(Producer#+this.numbern +put:+i);n try n sleep(int)(Math.random()*100);n catch(InterruptedException e)n n nnpublic class CubbyHole
35、 n private int contents;n private boolean available=false;n public synchronized int get()n while(available=false)n try n wait();n catch(InterruptedException e)n n available=false;n notifyAll();n return contents;n n public synchronized void put(int value)n while(available=true)n try n wait();n catch(
36、InterruptedException e)n n contents=value;n available=true;n notifyAll();n nnpublic class Consumer extends Thread n private CubbyHole cubbyhole;n private int number;n public Consumer(CubbyHole c,int number)n cubbyhole=c;n this.number=number;n n public void run()n int value=0;n for(int i=0;i 10;i+)n
37、value=cubbyhole.get();n System.out.println(Consumer#+this.numbern +got:+value);n n nnclass ProducerConsumerTestn n public static void main(String args)n n CubbyHole c=new CubbyHole();n Producer p1=new Producer(c,1);n Consumer c1=new Consumer(c,1);n p1.start();n c1.start();n nn运行结果如图运行结果如图6-12所示。所示。n
38、图6-12 运行结果(例6.13)n本程序就好比一个卧底与一个情报人员专门约定在一个树洞通信一样。当卧底往树洞里投放了情报后,只有等着情报员将情报拿走了,才能继续往树洞里面投放情报,否则只有等待。n程序中定义了三个类,Producer、Consumer和CubbyHole三个类。n值得注意的是,Producer类和Consumer类所调用CubbyHole类的put和get方法都是同步方法,这样就保证了卧底和情报员在情报的投放和领取上保持了一致。n本节举例介绍如何设置线程的优先级。程序如下。n【例6.14】设置线程优先级。例子详见书188191页n运行的结果如图6-13所示。n图6-13 运行
39、结果(例6.14)nThreadLocal类中有以下三个方法:类中有以下三个方法:nObject get()。检索变量的当前线程的值。nprotected Object initialValue()。可选的,如果线程未使用过某个变量,那么可以用这个方法来设置这个变量的初始值;它允许延迟初始化。nvoid set(Object value)。修改当前线程的值。n【例例6.15】ThreadLocal的具体应用举例,程序如下。的具体应用举例,程序如下。nimport java.lang.ThreadLocal;nimport java.util.*;nclass DebugLogger n pri
40、vate static class ThreadLocalList extends ThreadLocal n public Object initialValue()n return new ArrayList();n n public List getList()n return(List)super.get();n n n private ThreadLocalList list=new ThreadLocalList();n private static String stringArray=new String0;n public void clear()n list.getList
41、().clear();n n public void put(String text)n list.getList().add(text);n n public String get()n list.getList().toArray(stringArray);n return stringArray;n nn1.填空题n(1)可以通过可以通过_和和_来编写一个线程类。来编写一个线程类。n(2)线程有线程有_、_、_和和_状态。状态。n2.选择题n(1)一个线程想让另一个线程不能执行,它对第二个线程调用一个线程想让另一个线程不能执行,它对第二个线程调用yield()方法,能实现吗方法,能实现吗
42、?()nA.True B.Falsen(2)一个线程的一个线程的run()方法代码如下:方法代码如下:ntryn sleep(100);ncatch(InterruptedException e)n假设线程没有被中断,下列为真的是假设线程没有被中断,下列为真的是()。nA.代码不会被编译,因为异常不会在线程的代码不会被编译,因为异常不会在线程的run()方法中捕获方法中捕获nB.在代码的第在代码的第2 行,线程将停止运行,至多行,线程将停止运行,至多100ms 后恢复执行后恢复执行nC.在代码的第在代码的第2 行,线程将停止运行,恰好在行,线程将停止运行,恰好在100ms 恢复执行恢复执行nD
43、.在代码的第在代码的第2 行,线程将停止运行,在行,线程将停止运行,在100ms 后的某个时间恢复执后的某个时间恢复执行行n3.判断题n(1)C和和Java都是多线程语言。都是多线程语言。()n(2)如果线程死亡,它便不能运行。如果线程死亡,它便不能运行。()n4.简答题n(1)创建线程的两种方法是什么?创建线程的两种方法是什么?n(2)线程的四种状态是什么?线程的四种状态是什么?n5.操作题n(1)编写一个编写一个Race类,它模拟兔子和乌龟之间的赛跑。类,它模拟兔子和乌龟之间的赛跑。用用Math.random()方法使比赛更有趣。方法使比赛更有趣。n(2)编写一个编写一个StackTest类,其中实现后进先出的数据类,其中实现后进先出的数据结构,并且线程是安全的,不会产生死锁。结构,并且线程是安全的,不会产生死锁。