Java程序设计17——多线程-Part-A
1 多线程
1.1 简介
大部分时候,我们编写的程序都是单线程的。也就是都只有一条顺序执行流:程序从main方法开始执行,依次向下执行每行代码,如果程序执行某行代码遇到了阻塞,则程序会停滞在该处。如果我们使用IDE工具的单步调试功能,将可以很清楚的看出这一点。
实际情况是,单线程的程序往往功能非常有限,例如我们需要开发一个简单的服务器程序,这个服务器程序需要面向不同客户端提供服务时,不同客户端之间应该互不干扰,否则会让客户端不停的等待一次请求完,而其他的客户端在等待。多线程听上去非常专业概念,其实非常简单:单线程的程序只有一个顺序执行流,多线程的程序可以包括多个顺序执行流,多个顺序流之间互不干扰。可以这样理解:单线程程序如果只雇佣一个服务员的餐厅,他必须做完一件事情后才可以做下一件事情;多线程程序则如同雇佣多个服务员的餐厅,他们可以同时进行着多件事情。
本章会详细介绍Java多线程编程的相关方面,包括创建、启动线程、控制线程以及多线程的同步操作,并介绍如何利用Java内建支持的线程池来提高多线程性能。
1.2 线程概述
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
1.2.1 线程和进程
几乎所有操作系统都支持进程的概念,所有运行中的任务通常对应一条进程(process)。当一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
一般而言,进程包括如下三个特征
1.独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2.动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
3.并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
大部分操作系统都支持多进程并发运行,现代的操作系统几乎都支持同时运行多个任务:例如我们可以一边开着开发工具写程序,一边还开着参考手册备查,同时还是用电脑播放音乐。除此之外,每台电脑运行时还有大量底层的支持性程序在运行。这些进程看上去是在同时工作的。
但事实的真相是,对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说只能运行一个进程,CPU不断地在这些进程之间轮换执行。那么为什么我们感觉不到任何中断现象呢?这是因为CPU的执行速度相对我们的感觉实在是太快了(当然,如果启动的程序足够多,我们依然可以感觉程序的运行速度下降),所以虽然CPU在多次进程之间轮换执行,但我们人类感觉好像多个进程在同时执行。
现代的操作系统都支持多进程的并发,但在具体的实现细节上可能因为硬件和操作系统的不同而采用不同的策略。比较常用的方式有:共用式的多任务操作策略目前操作系统大多采用效率更高的抢占式多任务策略。
多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程,线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每条线程也是相互独立的。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,到哪不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方面;但必须更加小心,我们必须确保线程不会妨碍同一进程里的其他线程。
线程可以完成一定的任务,可与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。
线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看做多个独立的应用,对多线程实现调度和管理及资源分配。线程的调度和管理由进程本身负责完成。
简而言之:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少包含一个线程。
1.3 多线程的优势
线程在程序中是独立的、并发的执行流,但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性搞。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据等,线程很容易实现相互之间的通信。
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多进程实现并发性能要高得多。
总结起来,使用多线程编程包含如下几个优点:
1.进程间不能共享内存,但线程之间共享内存非常容易
2.系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
3.Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
在实际应用中,多线程是非常有用的,一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收;图形用户界面GUI也需要启动单独的线程来从主机环境手机用户界面事件。总之,多线程在实际编程中的应用是非常广泛的。
线程的创建和启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每条线程的作用是完成一定任务的,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用run方法来封装这样一段程序流。
2 创建线程的两种方法
2.1 通过继承Thread类来创建启动多线程的步骤
1.定义Thread类的子类,并重写该类的run方法,该run方法的方法体就是代表了线程需要完成的任务。因此,我们经常把run方法称为线程执行体。
2.创建Thread子类的实例,即是创建了线程对象
3.用线程对象的start方法来启动该线程
下面程序示范了通过继承Thread类来创建、并启动多线程的程序。
package chapter16; //通过继承Thread类来创建线程类 public class FirstThread extends Thread{ private int i; //重写run方法,run方法的方法体,就是线程的执行体 public void run(){ for(; i < 100; i++){ //当线程类继承Thread类时,通过getName()方法来获得线程的名称 //如果想获取当前线程,直接使用this即可 System.out.println(this.getName() + " " + i); } } public static void main(String[] args){ for(int i = 0; i < 100; i++){ //调用Thread的currentThread方法获取当前进程 System.out.println(Thread.currentThread().getName() + " " + i); if(i == 27){ //创建并启动第一条线程 new FirstThread().run(); //创建并启动第二条线程 new FirstThread().run(); } } } } 运行结果: main 0 main 1 main 2 ....... main 22 main 23 main 24 main 25 main 26 main 27 Thread-0 0 Thread-0 1 Thread-0 2 Thread-0 3 Thread-0 4 Thread-0 5 Thread-0 6 Thread-0 7 ........ Thread-0 97 Thread-0 98 Thread-0 99 Thread-1 0 Thread-1 1 Thread-1 2 Thread-1 3 Thread-1 4 Thread-1 5 Thread-1 6 Thread-1 7 ......... Thread-1 97 Thread-1 98 Thread-1 99 main 28 main 29 main 30 main 31 ......
上面程序中FirstThread类继承了Thread类,并实现了run方法,如程序中的斜体字代码部分。该run方法里的代码执行流就是该线程所需完成的任务。程序的主方法也包含了一个循环,当循环变量i等于27时创建并启动两条新线程。程序的执行结果见上。
虽然上面程序只显式地创建并启动两条线程,但实际上程序至少有3条线程:程序显式创建的2个子线程和主线程。前面已经提到的,当Java程序开始运行后,程序至少会创建一条主线程,主线程的线程执行体不是由run方法来确定的,而是由main方法来确定:main方法的方法体代表主线程的线程执行体。
进行多线程编程时,不要忘记了java程序运行时默认的主线程,main方法的方法体就是主线程的线程执行体。
除此之外,上面程序还用到了线程两个方法:
1.Thread.currentThread():currentThread是Thread类的静态方法,该方法总是返回当前正在执行的线程对象
2.getName():该方法是Thread的实例方法,该方法返回调用该方法的线程的名字。
程序可以通过setName(String name)方法为线程设置名字,也可以通过getName()方法返回指定线程的名字。默认情况下,主线程的名字为main,用户启动的多条线程的名字依次为Thread-0、Thread-1、Thread-2....Thread-n等。
Thread-0和Thread-1两条线程输出的i变量不连续————注意i变量是FirstThread的实例属性,而不是局部变量,但因为程序每次创建线程对象都需要创建一个FirstThread对象,所以Thread-0和Thread-1不能共享该实例属性。
使用几次Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。这会导致一个问题,比如说买票的时候,假设仅剩1张票了,两个客户端同时请求购买,由于没有共享实例变量,两边的客户端并不知道别人也在购买,所以大家都以为还有1张票,当一个客户端成功购买一张票后,也就是票理应变为0,但由于没有共享线程类的资源(变量),都还以为有1张票,而实际票已经卖完了,这就会导致问题。
2.2通过实现Runnable接口创建线程
实现Runnable接口来创建并启动多条线程的步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。代码如下所示:
//创建Runnable实现类的对象 SecondThread st = new SecondThread(); //以Runnable 实现类的对象作为Thread的target来创建Thread对象,即线程对象 new Thread(st); 也可以在创建Thread对象时为该Thread对象指定一个名字,代码如下所示: //创建Thread对象时指定target和新线程名字 new Thread(st, "新线程1");
Runnable对象仅仅作为Thread对象 的target,Runnable实现类里包含了run方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run方法。处于这种考虑,Thread类是否可以把任意对象的任意方法作为线程执行体?Java语言是不可以的,Java语言的Thread必须使用Runnable对象的run方法作为线程执行体;但C#可以把任何对象的任意方法来作为线程执行体。
2.2 调用线程对象的start方法来启动线程
下面程序示范了通过实现Runnable接口来创建并启动多线程的程序
1 class ThreadDemo004 implements Runnable{ 2 public void run(){ 3 for( int i = 5 ;i < 100&i > 0;i--){ 4 System.out.println("余票:" + i); 5 } 6 } 7 }; 8 9 public class ThreadDemo003 { 10 public static void main(String args[]){ 11 ThreadDemo004 td1 = new ThreadDemo004(); 12 new Thread(td1).start(); 13 new Thread(td1).start(); 14 } 15 }; 16 17 上述程序运行结果是: 18 19 余票:5 20 余票:4 21 余票:3 22 余票:2 23 余票:1 24 余票:5 25 余票:4 26 余票:3 27 余票:2 28 余票:1
也就是没有实现多线程的资源共享,这个例子中,第一个例子开启了一个进程i是局部变量,属于第一个进程的,不可能共享,需要用下面的程序实现。
1 class ThreadDemo004 implements Runnable{ 2 3 private int ticket = 5; 4 public void run(){ 5 for( int i = 5 ;i < 100;i++){ 6 if(ticket > 0){ 7 System.out.println("余票:" + ticket--); 8 } 9 } 10 } 11 }; 12 13 public class ThreadDemo003 { 14 15 public static void main(String args[]){ 16 ThreadDemo004 td1 = new ThreadDemo004(); 17 new Thread(td1).start(); 18 new Thread(td1).start(); 19 } 20 };
上面的run方法实现了Runnable的run方法,也就是定义了该线程的执行体。对比FirstThread中的run方法体和SecondThread的run方法体可以发现:使用继承Thread时获得当前线程对象比较简单:直接使用this就可以了;但使用实现Runnable接口时要获得当前线程对象必须使用Thread.currentThread()方法。另外启动线程时候要使用start方法来启动这两个线程,start启动方式也只是封装了run方法。
2.3 两种方式所创建线程的对比
通过继承Thread类或实现Runnable接口都可以实现多线程,但两种方式存在一定差别,相比之下两种方式的主要差别如下。
采用实现Runnable接口方式的多线程
1.线程类只是实现了Runnable接口,还可以继承其他类。
2.在这种方式下,可以多给线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形参清晰的模型,较好地体现了面向对象思想。
3.必须使用Thread.currentThread()方法
采用集成Thread类方式的多线程
1.劣势是:因为线程类已经继承了Thread类,所以不能再继承其他父类。
2.无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
几乎所有应用都采用第一种,也就是实现Runnable接口的方式。
3 线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的声明周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。尤其是当线程启动以后,它不能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
3.1 新建和就绪状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行进程执行体中的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,它只是表示该线程可以运行了,至于该线程何时开始运行,取决于JVM里线程调度器的调度。
启动线程使用start方法,而不是run方法,永远不要调用线程对象的run方法,调用start方法来启动线程,系统会把该run方法当初线程执行体来处理。但如果直接调用线程对象的run方法,则run方法立即就会被执行,而且在run方法返回之前其他线程无法并发执行————也就是说系统把线程对象当成一个普通对象,而run方法也是一个普通方法,而不是线程执行体
1 package chapter16; 2 3 public class InvokeRun extends Thread{ 4 private int i ; 5 //重写run方法,run方法的方法体就是线程执行体 6 public void run(){ 7 for ( ; i < 100 ; i++ ){ 8 //直接调用run方法时,Thread的this.getName返回的是该对象名字, 9 //而不是当前线程的名字。 10 //使用Thread.currentThread().getName()总是获取当前线程名字 11 System.out.println(Thread.currentThread().getName() + " " + i); 12 } 13 } 14 15 public static void main(String[] args){ 16 for (int i = 0; i < 100; i++){ 17 //调用Thread的currentThread方法获取当前线程 18 System.out.println(Thread.currentThread().getName() + " " + i); 19 if (i == 20){ 20 //直接调用线程对象的run方法, 21 //系统会把线程对象当成普通对象,run方法当成普通方法, 22 //所以下面两行代码并不会启动2条线程,而是依次执行2个run方法 23 new InvokeRun().run(); 24 new InvokeRun().run(); 25 } 26 } 27 } 28 } 29 运行结果: 30 main 0 31 main 1 32 main 2 33 ...... 34 main 26 35 main 27 36 main 0 37 main 1 38 main 2 39 main 3 40 ...... 41 main 97 42 main 98 43 main 99 44 main 0 45 main 1 46 main 2 47 ...... 48 main 97 49 main 98 50 main 99 51 main 28 52 main 29 53 ....... 54 main 98 55 main 99
上面程序线程对象后直接调用了线程对象的run方法,程序运行的结果是整个程序只有一条线程:主线程。还有一点要指出的是,如果直接调用线程对象的run方法,则run方法里不能直接通过getName()方法来获得当前执行线程的名字,而是需要使用Thread.currentThread()方法先获得当前线程,再调用线程对象的getName()方法来获得线程名字。
注意:方法和线程名称的对应关系,重写的run方法是一个线程执行体,但线程的名称并不是run,也不是run所在的对象的名称,而是由JVM来安排的,当然使用实现Runnable接口的方式也可以自定义线程名称,但是总之线程的名称和run方法名和run所在对象的名称没有关系。不过main方法的执行体对应的进程名称就是main,线程名称也是main,这是特殊的地方。
不要对已经处于启动状态的线程再次调用start方法,否则将引发IllegalThreadStateException
注意:有时运行多线程时候,当主线程在i等于20时调用了子线程的start方法来启动当前线程,但当前线程并没有立即执行,而是等到主线程为22时才看到子线程开始执行(执行程序时不一定是22时切换,这种切换由底层平台控制,具有一定的随机性)。
如果程序希望调用子线程的start方法后子线程立即开始执行,程序可以使用Thread.sllep(1)来让当前运行的线程(主线程)睡眠1毫秒————1毫秒就足够,因为在这1毫秒内CPU不会空闲,它就会去执行另一条就绪状态的线程,这样就可以让我们的子线程立即获得执行。
3.2 运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,在任何时刻只有一条线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行(注意是并行:parallel)执行;但当线程数大于处理器数时,依然会有多条线程在同一个CPU上轮换的现象。
当一条线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行机会,线程调度的细节取决于底层平台采用何种策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完,系统就会剥夺该线程所占据的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
所有现代的桌面和服务器操作系统都采用的是抢占式调度策略,但一些小型设备如手机可能采用协作式调度,在这样的系统,只有当一个线程调用了它的sleep或yield方法后才会放弃所占用的资源————也就是必须由该线程主动放弃所占用的资源。
当发生如下情况下,线程将会进入阻塞状态:
1.线程调用sleep方法主动放弃所占用的处理器资源。
2.线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
3.线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识将在后面深入介绍。
4.线程在等待某个通知(notify)
5.程序调用了线程的suspend方法将该线程挂起。不过这个方法容易导致死锁,所以程序应该尽量避免使用该方法。
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会了。被阻塞的线程会在合适时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说呗阻塞的线程的阻塞解除后,必须重新等待线程调度器再次调度它。
针对上面的几种情况,当发生如下特定的情况即可用解除上面的阻塞,让线程重新进入就绪状态:
1.调用sleep方法的线程经过了指定时间。
2.线程调用的阻塞式IO方法已经返回。
3.线程成功地获得了试图取得同步监视器
4.线程正在等待某个通知时,其他线程发错了一个通知
5.处于关起状态的线程被调用了resume恢复方法。
从上图可以看出,线程从阻塞状态只能进入就绪状态,无法进行运行状态的。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所导致的,当就绪状态的线程获得处理器资源时,该线程进入运行状态;当运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()可以让当前处于运行状态的线程转入就绪状态,关于yield方法在后面有更详细介绍。
3.3 线程死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:
1.run()方法执行完成,线程正常结束。
2.线程抛出一个未捕获的Exception或Error
3.直接调用该线程的stop()方法来结束该线程————该方法容易导致死锁,通常不推荐使用。
当主线程结束后,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会手主线程的影响。
为了测试某条线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法就返回true;当线程处于新建、死亡两种状态时,该方法返回false。
不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。
1 package chapter10; 2 3 public class StartDead extends Thread{ 4 private int i; 5 //重写run方法,该方法体就是线程的执行体 6 public void run(){ 7 for(; i < 100; i++){ 8 //当线程类继承Thread类时,通过getName()方法来获得线程的名称 9 //如果想获取当前线程,直接使用this即可 10 System.out.println(Thread.currentThread().getName() + " " + i); 11 } 12 } 13 public static void main(String[] args){ 14 //创建线程对象 15 StartDead sd = new StartDead(); 16 for(int i = 0; i < 200; i++){ 17 //调用Thread的currentThread方法获取当前线程 18 System.out.println(Thread.currentThread().getName() + " " + i); 19 if(i == 20){ 20 //启动线程 21 sd.start(); 22 //判断当前线程的状态 23 System.out.println(sd.isAlive()); 24 } 25 //在i == 20时候,线程是启动的,所以20以后线程将死亡 26 //因此不能再次启动线程 27 if(i > 20 && (sd.isAlive())){ 28 //试图再次启动该线程 29 sd.start(); 30 } 31 } 32 } 33 } 34 Exception in thread "main" java.lang.IllegalThreadStateException
上述代码在线程已死亡情况下,再次调用start()方法来启动线程,运行上面程序将引发IllegalThreadStateException异常,表名死亡状态的线程无法再次运行了。
3.4 控制线程
java的线程支持提供了一些便捷的工具方法,通过这些便捷的工具方法可以很好地控制线程的执行。
3.4.1 join线程
Thread提供了让一个线程等待另一个线程完成的方法:join()。当在某个程序执行流中调用其他线程的join()方法时,主调用线程将被阻塞,等待被调用线程执行,直到被join方法加入的join线程执行完成为止。
join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程进一步操作。
1 package chapter16; 2 3 public class JoinThread extends Thread{ 4 //提供一个有参数的构造器,用于设置线程的名字 5 public JoinThread(String name){ 6 super(name); 7 } 8 //重写run方法,定义线程执行体 9 public void run(){ 10 for(int i = 0; i < 100; i++){ 11 System.out.println(Thread.currentThread().getName() + i); 12 } 13 } 14 public static void main(String[] args) throws InterruptedException{ 15 //启动子线程 16 new JoinThread("新线程").start(); 17 for(int i = 0; i < 200; i++){ 18 if(i == 20){ 19 JoinThread jt = new JoinThread("被join的线程"); 20 jt.start(); 21 //main线程调用了jt的join方法,必须等jt线程执行完 22 //main线程才能继续执行,否则的话main线程和其他子线程 23 //是并发执行的 24 jt.join(); 25 } 26 System.out.println(Thread.currentThread().getName() +" " + i); 27 } 28 } 29 } 30 输出结果: 31 main 0 32 main 1 33 main 2 34 ...... 35 main 17 36 main 18 37 main 19 38 被join的线程0 39 被join的线程1 40 被join的线程2 41 被join的线程3 42 被join的线程4 43 被join的线程5 44 被join的线程6 45 被join的线程7 46 ...... 47 被join的线程96 48 被join的线程97 49 被join的线程98 50 被join的线程99 51 main 20 52 main 21 53 main 22 54 ...... 55 main 196 56 main 197 57 main 198 58 main 199 59 新线程0 60 新线程1 61 ....... 62 新线程97 63 新线程98 64 新线程99
上面程序中一共有3条线程,主方法开始就启动了名为"新线程"的子线程,该子线程将会和main线程并发执行。当主线程的循环变量i等于20时,启动了名为"被join的线程"的线程,该线程不会和main线程并发执行,而是main线程必须等该线程执行结束后才可以向下执行。在名为"被Join的线程"的线程执行时,实际上只有2条子线程并发执行,而主线程处于等待状态。运行上面程序看到上面的运行结果
main 18 main 19 被join的线程0 被join的线程1
在i = 20时候,main线程被阻塞,直到被join的线程执行完成。
join方法又三种重载的形式
1.join():等待被join的线程执行完成。
2.join(long millis):等待被join的线程的时间最长为millis毫秒,如果在millis毫秒内,被join的线程还没有执行结束,则不再等待。
3.join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒加上nanos微秒(千分之一毫秒)
通常我们很少使用第三个方法,原因有两个:程序对时间的精度无须精确到千分之一毫秒!计算机硬件、操作系统本身也无法精确到千分之一毫秒。
3.5 后台线程
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为"后台线程" ,又称为守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。下面程序将执行线程设置成后台线程,将可以看到当所有前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。
1 package chapter16; 2 3 public class DaemonThread extends Thread { 4 //定义后台线程的执行体,与前台线程执行体一样 5 public void run(){ 6 for(int i = 0; i < 1000; i++){ 7 //当线程类继承Thread类时,通过getName()方法来获得线程的名称 8 //如果想获取当前线程,直接使用this即可 9 System.out.println(Thread.currentThread().getName() + " " + i); 10 } 11 } 12 public static void main(String[] args){ 13 //创建线程对象 14 DaemonThread dt = new DaemonThread(); 15 //将此线程设置成后台线程 16 dt.setDaemon(true); 17 //启动后台线程 18 dt.start(); 19 for(int i = 0; i < 10; i++){ 20 //当线程类继承Thread类时,通过getName()方法来获得线程的名称 21 //如果想获取当前线程,直接使用this即可 22 System.out.println(Thread.currentThread().getName() + " " + i); 23 } 24 //程序执行到此,前台main线程执行结束,后台线程也结束 25 } 26 } 27 输出结果: 28 main 0 29 main 1 30 main 2 31 main 3 32 main 4 33 main 5 34 main 6 35 main 7 36 main 8 37 main 9 38 Thread-0 0 39 Thread-0 1 40 Thread-0 2 41 Thread-0 3 42 ....... 43 Thread-0 327 44 Thread-0 328 45 Thread-0 329 46 Thread-0 330 47 Thread-0 331 48 Thread-0 332 49 Thread-0 333 50 Thread-0 334 51 Thread-0 335 52 Thread-0 336 53 Thread-0 337 54 Thread-0 338 55 Thread-0 339 56 Thread-0 340 57 Thread-0 341 58 Thread-0 342
上面程序中粗体字代码先将t线程设置为后台线程,然后启动该线程,本来该线程应该执行到i等于999时才会结束,但运行程序时不难发现后台线程无法运行到999,因为当主线程,也就是程序中唯一的前台线程运行结束后,JVM会主动退出,因而后台线程也就被结束了。
Thread类还提供了一个isDaemon()方法,用于判断指定线程是否为后台线程。
从上面程序可以看出,主线程默认是前台线程,dt线程默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认是后台线程:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令,到它做出响应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。
3.6 线程的睡眠:sleep方法
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法,sleep方法有两种重载的形式:
1.static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度的影响。
2.static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度的影响
与前面类似的是,程序很少调用第二种形式的sleep方法
当当前线程调用sleep方法进入阻塞状态后,在其sleep时间段内,该线程不会获得执行的机会,即使系统中没有其他可运行的线程,处于sleep中的线程也不会运行,因此sleep方法常用来暂停程序的执行。
下面程序调用sleep方法来暂停主线程的执行,因为该程序只有一条主线程,当主线程进入sleep后,系统没有可执行的线程,所以可以看到程序在sleep处暂停。
1 package chapter16; 2 3 import java.util.*; 4 5 public class TestSleep { 6 public static void main(String[] args)throws Exception{ 7 for(int i = 0; i < 7; i++){ 8 System.out.println("当前时间: " + new Date()); 9 Thread.sleep(1000); 10 } 11 } 12 } 13 14 上面程序将当前执行的线程暂停1秒,输出10条字符串,字符串之间间隔时间为1秒 15 输出结果: 16 当前时间: Sat Jul 20 10:01:57 CST 2013 17 当前时间: Sat Jul 20 10:01:58 CST 2013 18 当前时间: Sat Jul 20 10:01:59 CST 2013 19 当前时间: Sat Jul 20 10:02:00 CST 2013 20 当前时间: Sat Jul 20 10:02:01 CST 2013 21 当前时间: Sat Jul 20 10:02:02 CST 2013 22 当前时间: Sat Jul 20 10:02:03 CST 2013
4 线程让步:yield
yield()方法是一个和sleep方法有点相似的方法,它也是一个Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
实际上,当某个线程调用了yield方法暂停之后,只有优先级当前线程相同个,或者优先级比当前线程更高的就绪状态的线程才会获得执行的机会。
1 package chapter16; 2 3 public class TestYield extends Thread{ 4 public TestYield(){ 5 6 }; 7 public TestYield(String name){ 8 super(name); 9 }; 10 //定义run方法的执行体 11 public void run(){ 12 for(int i = 0; i < 27; i++){ 13 System.out.println(Thread.currentThread().getName() + " " + i); 14 //当i = 20进行线程让步yield 15 if(i == 20){ 16 Thread.yield(); 17 } 18 } 19 }; 20 public static void main(String[] args){ 21 //启动两条并发线程 22 TestYield ty1 = new TestYield("高级"); 23 //将ty1的线程级别设置成高级 24 //ty1.setPriority(MAX_PRIORITY); 25 ty1.start(); 26 TestYield ty2 = new TestYield("低级"); 27 //将ty2的线程级别设置成低级 28 //ty2.setPriority(MIN_PRIORITY); 29 ty2.start(); 30 } 31 } 32 输出结果: 33 低级 0 34 低级 1 35 低级 2 36 低级 3 37 低级 4 38 低级 5 39 低级 6 40 低级 7 41 低级 8 42 低级 9 43 低级 10 44 低级 11 45 低级 12 46 低级 13 47 低级 14 48 低级 15 49 低级 16 50 低级 17 51 低级 18 52 低级 19 53 低级 20 54 高级 0 55 高级 1 56 高级 2 57 高级 3 58 高级 4 59 高级 5 60 高级 6 61 高级 7 62 高级 8 63 高级 9 64 高级 10 65 高级 11 66 高级 12 67 高级 13 68 高级 14 69 高级 15 70 高级 16 71 高级 17 72 高级 18 73 高级 19 74 高级 20 75 低级 21 76 低级 22 77 低级 23 78 低级 24 79 低级 25 80 低级 26 81 高级 21 82 高级 22 83 高级 23 84 高级 24 85 高级 25 86 高级 26
取消上面的线程优先级注释,再运行一次
1 package chapter16; 2 3 public class TestYield extends Thread{ 4 public TestYield(){ 5 6 }; 7 public TestYield(String name){ 8 super(name); 9 }; 10 //定义run方法的执行体 11 public void run(){ 12 for(int i = 0; i < 27; i++){ 13 System.out.println(Thread.currentThread().getName() + " " + i); 14 //当i = 20进行线程让步yield 15 if(i == 20){ 16 Thread.yield(); 17 } 18 } 19 }; 20 public static void main(String[] args){ 21 //启动两条并发线程 22 TestYield ty1 = new TestYield("高级"); 23 //将ty1的线程级别设置成高级 24 ty1.setPriority(MAX_PRIORITY); 25 ty1.start(); 26 TestYield ty2 = new TestYield("低级"); 27 //将ty2的线程级别设置成低级 28 ty2.setPriority(MIN_PRIORITY); 29 ty2.start(); 30 } 31 } 32 输出结果: 33 高级 0 34 高级 1 35 高级 2 36 高级 3 37 高级 4 38 高级 5 39 高级 6 40 高级 7 41 高级 8 42 高级 9 43 高级 10 44 高级 11 45 高级 12 46 高级 13 47 高级 14 48 高级 15 49 高级 16 50 高级 17 51 高级 18 52 高级 19 53 高级 20 54 高级 21 55 高级 22 56 高级 23 57 高级 24 58 高级 25 59 高级 26 60 低级 0 61 低级 1 62 低级 2 63 低级 3 64 低级 4 65 低级 5 66 低级 6 67 低级 7 68 低级 8 69 低级 9 70 低级 10 71 低级 11 72 低级 12 73 低级 13 74 低级 14 75 低级 15 76 低级 16 77 低级 17 78 低级 18 79 低级 19 80 低级 20 81 低级 21 82 低级 22 83 低级 23 84 低级 24 85 低级 25 86 低级 26
分析上面两个程序,第一个程序,优先级设置代码被注释的,也就是两条线程的优先级完全一样,所以系统当一条线程使用yield()方法暂停之后,另一条线程就会开始执行。所以两条线程是交替执行的。
第二个程序优先级设置代码没有注释,也就是可以被设置成不同优先级,虽然第一条线程使用了yield方法暂停,但由于第二条线程的优先级依然没有它高,所以从就绪状态转成执行状态。
4.1 关于sleep方法和yield方法的区别如下
1.sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级,但yield方法只会给优先级相同,或优先级更改的线程执行机会。
2.sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态,而yield不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态,因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源被执行。
3.sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时要么捕捉该异常,要么显示声明抛出该异常,而yield方法则没有声明抛出任何异常。
4.sleep方法比yield方法又更好的可移植性,通常不要依靠yield来控制并发线程的执行。