Java多线程编程.docx
- 文档编号:11354114
- 上传时间:2023-02-28
- 格式:DOCX
- 页数:33
- 大小:149.97KB
Java多线程编程.docx
《Java多线程编程.docx》由会员分享,可在线阅读,更多相关《Java多线程编程.docx(33页珍藏版)》请在冰豆网上搜索。
Java多线程编程
模块十四线程
模块十四线程1
1.Java中的线程?
2
1.1.线程的三个部分2
1.2.创建线程2
1.3.启动线程3
1.4.线程调度4
2.线程的基本控制5
2.1.终止一个线程5
2.2.测试一个线程6
3.创建线程的其它方法7
3.1.使用那种方法?
7
4.使用Java技术中的synchronized8
4.1.问题8
4.2.对象锁标志9
4.3.synchronized――放在一起10
4.4.线程状态11
4.5.死锁11
5.线程交互-wait()和notify()11
5.1.背景11
5.2.问题11
5.3.解决方案12
5.4.wait()和notify()12
5.5.WaitPool12
5.6.线程状态12
5.7.同步的监视模型13
6.放在一起13
6.1.SyncStack类14
6.2.SyncStack完整的代码15
7.JDK1.2中的线程控制18
7.1.suspend()和resume()方法18
7.2.stop()方法19
7.3.合适的线程控制19
8.练习:
使用多线程编程20
8.1.创建三个线程20
8.2.使用动画20
8.3.检查你的进度20
本模块讨论多线程,它允许一个程序同时执行多个任务。
在完成本模块的学习后,你应当能够:
●定义一个线程
●在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据
●控制线程的执行,并用线程编写独立于平台的代码
●描述在多个线程共享数据时可能会碰到的困难
●使用synchronized关键字保护数据不受破坏
●使用wait()和notify()使线程间相互通信
●解释为什么不推荐使用suspend()、resume()和stop()方法?
1.Java中的线程?
一个关于计算机的简化的视图是:
它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。
在这个简化视图中,只能执行一个作业。
一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。
你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。
如果你要执行一个以上的作业,这类似有一台以上的计算机。
在这个模型中,线程(或执行上下文),被认为是带有自己的程序代码和数据的虚拟处理机的封装。
java.lang.Thread类允许用户创建并控制他们的线程。
1.1.线程的三个部分
进程是正在执行的程序。
一个或更多的线程构成了一个进程。
一个线程(或执行上下文)由三个主要部分组成
●一个虚拟CPU
●CPU执行的代码
●代码操作的数据
代码可以由多个线程共享,它不依赖数据。
如果两个线程执行同一个类的实例的代码时,则它们可以共享相同的代码。
类似地,数据可以由多个线程共享,而不依赖代码。
如果两个线程共享对一个公共对象的访问,则它们可以共享相同的数据。
在Java编程中,虚拟处理机封装在Thread类的一个实例里。
构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。
1.2.创建线程
本节介绍如何创建线程,以及如何使用构造函数参数来为一个线程提供运行时的数据和代码。
一个Thread类构造函数带有一个参数,它是Runnable的一个实例。
一个Runnable是由一个实现了Runnable接口(即,提供了一个publicvoidrun()方法)的类产生的。
例如:
1.publicclassThreadTest{
2.publicstaticvoidmain(Stringargs[]){
3.HelloRunnerr=newHelloRunner();
4.Threadt=newThread(r);
5.t.start();
6.}
7.}
8.
9.classHelloRunnerimplementsRunnable{
10.inti;
11.
12.publicvoidrun(){
13.i=0;
14.while(true){
15.System.out.println("Hello"+i++);
16.if(i==50)break;
17.}
18.}
19.}
首先,main()方法构造了HelloRunner类的一个实例r。
实例r有它自己的数据,在这里就是整数i。
因为实例r是传给Thread类的构造函数的,所以r的整数i就是线程运行时刻所操作的数据。
线程总是从它所装载的Runnable实例(在本例中,这个实例就是r。
)的run()方法开始运行。
一个多线程编程环境允许创建基于同一个Runnable实例的多个线程。
这可以通过以下方法来做到:
Threadt1=newThread(r);
Threadt2=newThread(r);
此时,这两个线程共享数据和代码。
总之,线程是Thread对象的一个实例(或引用)。
线程从装入的Runnble实例的run()方法开始执行。
线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。
1.3.启动线程
一个新创建的线程并不自动开始运行。
你必须调用它的start()方法。
例如,你可以发现上例中第4行代码中的命令:
t.start();
调用start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。
这并不意味着线程就会立即运行。
1.4.线程调度
在Java中,线程是抢占式的,而不是分时的(一个常见的错误是认为“抢占式”只不过是“分时”的一种新奇的称呼而已)。
抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。
这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。
对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。
一个线程可能因为各种原因而不再是可运行的。
线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间。
这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。
所有可运行线程根据优先级保存在池中。
当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。
优先级最高的非空池中的线程会得到处理机时间(被运行)。
一个Thread对象在它的生命周期中会处于各种不同的状态。
下图形象地说明了这点:
线程进入“可运行”状态,并不意味着它立即开始运行。
在一个只带有一个CPU的机器上,在一个时刻只能进行一个动作。
(下节将描述:
如果有一个以上可运行线程时,系统如何分配CPU。
)
因为Java线程是抢占式的,所以你必须确保你的代码中的线程会不时地给其它线程运行的机会。
这可以通过在各种时间间隔中发出sleep()调用来做到。
1.publicclassRunnerimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//dolotsofinterestingstuff
5.
6.//Giveotherthreadsachance
7.try{
8.Thread.sleep(10);
9.}catch(InterruptedExceptione){
10.//Thisthread'ssleepwasinterrupted
11.//byanotherthread
12.}
13.}
14.}
15.}
注意try和catch块的使用。
Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。
线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。
注意Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。
sleep()的参数指定以毫秒为单位的线程最小休眠时间。
除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。
2.线程的基本控制
2.1.终止一个线程
当一个线程结束运行并终止时,它就不能再运行了。
可以用一个标志来指示run()方法,必须退出一个线程。
1.publicclassRunnerimplementsRunnable{
2.privatebooleantimeToQuit=false;
3.
4.publicvoidrun(){
5.while(!
timeToQuit){
6....
7.}
8.//cleanupbeforerun()ends
9.}
10.
11.publicvoidstopRunning(){
12.timeToQuit=true;
13.}
14.}
16.publicclassControlThread{
17.privateRunnabler=newRunner();
18.privateThreadt=newThread(r);
19.
20.publicvoidstartThread(){
21.t.start();
22.}
23.
24.publicvoidstopThread(){
25.//usespecificinstanceofRunner
26.r.stopRunning();
27.}
28.}
在一段特定的代码中,可以使用静态Thread方法currentThread()来获取对当前线程的引用,例如:
1.publicclassNameRunnerimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//lotsofinterestingstuff
5.}
6.//Printnameofthecurrentthread
7.System.out.println("Thread"+Thread.currentThread().getName()+"completed");
8.}
9.}
2.2.测试一个线程
有时线程可处于一个未知的状态。
2.2.1.isAlive()方法
isAlive()方法用来确定一个线程是否在运行。
活着的线程并不意味着线程正在运行;对于一个已开始运行但还没有完成任务的线程,这个方法返回true。
2.2.2.访问线程优先级
使用getPriority方法测定线程的当前优先级。
使用setPriority方法设定线程的当前优先级。
线程优先级是一个整数。
Thread类包含下列常数:
Thread.MIN_PRIORITY
Thread.NORM_PRIORITY
Thread.MAX_PRIORITY
2.2.3.延迟线程
存在可以使线程暂停执行的机制。
也可以恢复运行,就好象什么也没发生过一样,线程看上去就象在很慢地执行一条指令。
sleep()方法
sleep()方法是使线程停止一段时间的方法。
在sleep时间间隔期满后,线程不一定立即恢复执行。
这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非
(a)“醒来”的线程具有更高的优先级
(b)正在运行的线程因为其它原因而阻塞
1.publicclassXyzimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//lotsofinterestingstuff
5.//Printnameofthecurrentthread
6.System.out.println("Thread"+Thread.currentThread().getName()+"completed");
7.}
8.}
9.}
join()方法
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。
例如:
publicvoiddoTask(){
TimerThreadtt=newTimerThread(100);
tt.start();
...
//Dostuffinparallelwiththeotherthreadforawhile
...
//Waithereforthetimerthreadtofinish
try{
tt.join();
}catch(InterruptedExceptione){
//ttcamebackearly
}
...
//Nowcontinueinthisthread
...
}
可以带有一个以毫秒为单位的时间值来调用join方法,例如:
voidjoin(longtimeout);
其中join()方法会挂起当前线程。
挂起的时间或者为timeout毫秒,或者挂起当前线程直至它所调用的线程终止。
3.创建线程的其它方法
到目前为止,你已经知道如何用实现了Runnable的分离类来创建线程上下文。
事实上,这不是唯一的方法。
Thread类自身实现了Runnable接口,所以可以通过扩展Thread类而不是实现Runnable来创建线程。
1.publicclassMyThreadextendsThread{
2.publicvoidrun(){
3.while(running){
4.//dolotsofinterestingstuff
5.try{
6.sleep(100);
7.}catch(InterruptedExceptione){
8.//sleepinterrupted
9.}
10.}
11.}
12.
13.publicstaticvoidmain(Stringargs[]){
14.Threadt=newMyThread();
15.t.start();
16.}
17.}
3.1.使用那种方法?
建议你们实现Runnable接口。
3.1.1.实现Runnable的优点
●从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。
正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。
●由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。
在某些情况下,这会使你只能采用实现Runnable的方法。
●因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。
3.1.2.继承Thread的优点
●当一个run()方法体现在继承Thread的类中,用this指向实际控制运行的Thread实例。
因此,代码不再需要使用如下控制:
Thread.currentThread().join();
而可以简单地用:
join();
因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。
注意:
如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。
4.使用Java技术中的synchronized
本节讨论关键字synchronized的使用。
它提供Java编程语言一种机制,允许程序员控制共享数据的线程。
4.1.问题
想象一个表示栈的类。
这个类最初可能象下面那样:
1.publicclassMyStack{
3.intidx=0;
4.char[]data=newchar[6];
5.
6.publicvoidpush(charc){
7.data[idx]=c;
8.idx++;
9.}
10.
11.publiccharpop(){
12.idx--;
13.returndata[idx];
14.}
15.}
注意这个类没有处理栈的上溢和下溢,所以栈的容量是相当有限的。
这些方面和本讨论无关。
这个模型的行为要求索引值包含栈中下一个空单元的数组下标。
“先进后出”方法用来产生这个信息。
现在想象两个线程都有对这个类里的一个单一实例的引用。
一个线程将数据推入栈,而另一个线程,独立地,将数据弹出栈。
通常看来,数据将会正确地被加入或移走。
然而,这存在着潜在的问题。
假设线程a正在添加字符,而线程b正在移走字符。
线程a已经放入了一个字符,但还没有使下标加1。
因为某个原因,这个线程被剥夺运行的机会。
这时,对象所表示的数据模型是不一致的。
buffer|p|q|r||||
idx=2^
特别地,一致性会要求idx=3,或者还没有添加字符。
如果线程a恢复运行,那就可能不造成破坏,但假设线程b正等待移走一个字符。
在线程a等待另一个运行的机会时,线程b正在等待移走一个字符的机会。
pop()方法所指向的条目存在不一致的数据,然而pop方法要将下标值减1。
buffer|p|q|r||||
idx=1^
这实际上将忽略了字符“r”。
此后,它将返回字符“q”。
至此,从其行为来看,就好象没有推入字母“r”,所以很难说是否存在问题。
现在看一看如果线程a继续运行,会发生什么。
线程a从上次中断的地方开始运行,即在push()方法中,它将使下标值加1。
现在你可以看到:
buffer|p|q|r||||
idx=2^
注意这个配置隐含了:
“q”是有效的,而含有“r”的单元是下一个空单元。
也就是说,读取“q”时,它就象被两次推入了栈,而字母“r”则永远不会出现。
这是一个当多线程共享数据时会经常发生的问题的一个简单范例。
需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。
4.2.对象锁标志
在Java技术中,每个对象都有一个和它相关联的标志。
这个标志可以被认为是“锁标志”。
synchronized关键字使能和这个标志的交互,即允许独占地存取对象。
看一看下面修改过的代码片断:
publicvoidpush(charc){
synchronized(this){
data[idx]=c;
idx++;
}
}
当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。
4.2.1.对象锁标志
意识到它自身并没有保护数据是很重要的。
因为如果同一个对象的pop()方法没有受到synchronized的保护,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。
如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。
下图显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:
当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。
由于得不到标志,所以线程不能继续运行。
然后,线程加入到与那个对象锁相关联的等待线程池中。
当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。
4.2.2.释放锁标志
由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。
锁标志将自动返回给它的对象。
持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。
Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。
此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。
这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。
4.3.synchronized――放在一起
正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作用。
所有由synchronized块保护的易碎数据应当标记为private。
考虑来自对象的易碎部分的数据的可存取性。
如果它们不被标记为private,则它们可以由位于类定义之外的代码存取。
这样,你必须确信其他程序员不会省略必需的保护。
一个方法,如果它全部属于与这个实例同步的块,它可以把synchronized关键字放到它的头部。
下面两段代码是等价的:
publicvoidpush(charc){
synchronized(this){
...
}
}
publicsynchronizedvoidpush(charc){
...
}
如果你把synchronized作为一种修饰符,那么整个块就成为一个同步块。
这可能会导致不必要地持有锁标志很长时间,因而是低效的。
然而,以这种方式来标记方法可以使方法的用户由javadoc产生的文档了解到:
正在同步。
这对于设计时避免死锁(将在下一节讨论)是很重要的。
注意javadoc文档生成器将synchronized关键字传播到文档文件中,但它不能为在方法块内的synchronized(this)做到这点。
4.4.线程状态
同步是线程的特定状态,增加同步技术后,线程的状态图也要相应升级:
4.5.死锁
如果程序中有多个线程竞争多个资源,就可能会产生死锁。
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。
在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。
由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。
Java技术不监测也不试图避免这种情况。
因而保证不发生死锁就成了程序员的责任。
避免死锁的一个通用的经验法则是:
决定获取锁的次序并始终遵照这个次序。
按照与获取相反的次序释放锁。
5.线程交互-wait()和notify()
5.1.背景
把你自己和出租车司机当作两个线程。
你需要出租车司机带你到
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 多线程 编程