想要学习多线程,必须要先理解什么是并发什么是并行。
并行:是指两个或多个线程在同一时刻发生。
并发:是指两个或多个线程在同一时间段内发生。
为了方便理解多线程的概念,我们先举一个例子:
假如我们把公司看做是一个进程,那么人就是其中的线程。进程必须得有一个主线程,公司在创业初期往往可能出现一人打天下的现象,但是,至少得有一个人,公司才能运作。公司创业初期,业务还不算太多,往往就是老板一个人身兼数职,一天如果只有1、2趟活儿,应该还是忙得过来的。时间长了,随着业务的发展、口碑的建立,生意越来越兴隆,一个人肯定就忙不过来了。假设一天有5个活儿,老板一个人必须搬完A家才能搬B家,搬到黄昏估计也就搬到C家,D和E家都还在焦急地等待着呢。老板一个人要充当搬运工、司机、业务联系人、法人代表、出纳等众多角色,累死累活公司的规模也上不去,人手不够制约了公司的发展。那么怎么办,很简单,增加人手,然后这些人各司其职,同时工作,很块就处理完了公司的业务。而这些人手就是所谓的线程,开启了的线程可以并行运行。
多线程的好处:多线程相当于在一个进程中有多条不同的执行路径,在同一时刻进行执行,可以提高程序的执行效率。
多线程的应用场景:迅累多线程下载,数据库连接池,分批发送敦信等。
同步:相当于单线程,代码从上往下按照顺序执行。
异步:相当于多线程,开启一条新的路径执行,多条执行路径同时执行,互不影响。
一,进程和线程的概念
进程:是一个正在执行的程序。每一个进程都有一个执行顺序,该顺序是一个执行路径,或者叫做执行单元。进程是资源分配的基本单位(内存,进程ID(PID))。
线程:是一条执行路径,是进程内部的一个独立的执行单元,每个线程互不影响,一个进程至少有一个线程,线程控制进程的执行。线程是资源调度的单位。
多线程:在一个进程中多个线程并发执行。
1.1进程和线程的区别
♦内存区别:进程是有独立的内存空间,每一个进程之间是相互独立的,互不干扰。
线程除了有私有的内存空间外,还有共有的内存空间。
♦安全性:进程是相互独立的,一个进程的崩溃是不会影响到其他的进程,进程是安全的。
线程存在内存空间的共享,一个线程的崩溃可能会影响到其他线程,所以线程没有进程安全性高。
1.2进程和线程的关系
进程是相互独立的,一个进程下可以有一个或者多个线程。
Java中很少使用进程的概念,但可以使用下面代码来创建进程:
Runtime runtime=Runtime.getRuntime();//创建进程的方法
1.3Java默认有几个线程?
Java默认是有两个线程:主线程和垃圾回收的线程(Main和GC)
1.4java本身能否启动线程?
java本身是没有办法启动线程的,线程启动时需要调用底层操作系统的支持。Java通过调用本地方法,来调用c++编写的动态函数库,由c++去操作底层来启动线程。所以Java是通过间接的调用来启动线程的。
二,自定义线程的四种方式:1.继续Thread类,2.实现Runable接口,3.实现Callable接口的方法,4.线程池管理方法,另外还可以使用匿名对象。
第一种:继承Thread类
步骤: 1.定义类继承Thread类
2.重写Thread类中的run方法
为什么要重写run()方法? 将新定义的线程所要执行的代码存储在run()方法中,因为Thread类是用来描述线程的,而用于存储该线程运行时的代码的功能,就是由run()方法实现的,所以一般将新建的线程所要执行的代码,都放到run()方法中。注意主线程运行时的代码是存储在main()方法中的。(一般新建立的线程调用的start()方法,是由父类继承下来的,而start()方法中调用的run()方法,是被Thread子类重写过的方法。
3.调用线程的start()方法(该方法的作用:启动线程,并调用run方法)
1 class Demo extends Thread//1.继承Thread类 2 { 3 public void run() { //2.重写run方法 4 for(int i=0;i<10;i++){ 5 System.out.println("run..."+i); 6 } 7 } 8 } 9 public class TreadDemo { 10 public static void main(String[] args) {//main函数也是一个线程 11 Demo d=new Demo();//创建一个线程 12 d.start();//3.调用start方法 13 for(int i=0;i<20;i++){ 14 System.out.println("main...."+i); 15 } 16 } 17 }
运行结果部分图:
运行结果分析:
主线程(要执行代码存储在main()函数中)开始执行,当运行到这一句Demo d=new Demo(); 会新创建一个线程2,接着主线程调用新线程的start()方法,而start()方法内部又会区调用run()方法(这个新线程要执行的代码存储在run()方法中),此时新线程也开启了。而这一块打印结果为什么是main和run 的交替?因为当线程2开启后,只能说明线程2具备了运行条件,不一定立马就有cup的执行权,所以打印的结果先是main.....i ,这时线程2突然抢到了cup执行权,于是也进行了打印输出run.....i ,接着cup执行权又被主线程强走,然后打印main.....i ,所以打印结果就是他们的交替。下图是对上述代码执行过程的分析,要注意的是一个进程在开始至少有一个线程,而对于上面这个代码,刚开始的这个主线程就是由main()函数开启的。而且只要进程中还有线程未执行完毕,该进程就不会结束。
说到这一块可能就有人迷惑,那既然调用start()方法时,该方法会接着调用run()方法,那为什么不能直接调用run()方法来执行呢?要注意start()方法的作用不止调用run()方法,还用启动线程的作用。
先举一个直接调用run()方法的例子:
1 public static void main(String[] args) {//main函数也是一个线程 2 Demo d=new Demo();//创建一个线程 3 d.run();//3.调用start方法 4 for(int i=0;i<20;i++){ 5 System.out.println("main...."+i); 6 } 7 }
run()方法中的代码与上个例子中的相同。
执行结果:
不论运行多少次,都会发现结果和上图都是一样的。这和线程的随机性明显不符,为什么呢??(线程的随机性指的是多个线程的执行结果不唯一,谁抢到cup执行权谁执行)
因为当你直接调用run()方法时,虽然通过该句Demo d=new Demo()已创建新线程,但是并没有启动新线程!! 所以当主线程执行到这一句d.run(),因为新线程没有开启,所以run()方法中的内容是在主线程中执行了的,所以只有当run打印完,才会轮到main打印。
要注意,人们平时看到的多个线程在“同时”执行,其实在底层并不是多个线程同时一块执行的,而是通过快速的交替使用cup来执行自己的任务,因为其交替的速度非常快,快到人眼是感觉不到的,所以使我们在表面上看去,以为是多个线程在同时执行。这也就是为什么当我们电脑打开的程序也多时,电脑就会越卡。
补充:可通过Thread的getName()方法获得新线程的默认名字。 新线程的默认名字格式:Thread-0 编号从0开始。
//两种获得线程名字的方法
this.getName(); Thread.currentThread().getName();
Thread.currentThread()//可获得当前线程对象
那如何给新线程自定义名字呢?通过查资料,我们得知Thread有一个带参构造函数,所以我们可以直接在新建线程时,直接将名字赋给它。
Demo d=new Demo("one");//创建一个线程
要注意我们既然要使用它的带参构造函数,那么我们在子类中就必须定义一个带参构造函数。
1 class Demo extends Thread//1.继承Thread类 2 { 3 Demo(String name){ //定义一个带参构造函数。 4 super(name); 5 } 6 public void run() { //2.重写run方法 7 for(int i=0;i<10;i++){ 8 System.out.println(this.getName()+"run..."+i); 9 } 10 } 11 } 12 public class TreadDemo { 13 public static void main(String[] args) {//main函数也是一个线程 14 Demo d=new Demo("one");//创建一个线程 15 d.start();//3.调用start方法d.run() 16 for(int i=0;i<20;i++){ 17 System.out.println("main...."+i); 18 } 19 } 20 }
第二种:实现Runable接口(其实Thread也是实现Runnable接口的)
步骤:1.创建类实现Runnable接口
2.实现Runnable接口中的run()方法
目的:将线程执行的代码存储在run()方法中
3.通过Thread类建立线程对象
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为自定义的run()方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run()方法,就必须明确该run(方法的所属对象。
5.调用Thread类的start方法开启线程并调用Runnable接口子类的run()方法
实现方式和继承方式的区别:
1,实现方式避免了单继承的局限性。(因为一个类只能继承一个类,当继承了Thread类就无法在继承其他类,但因为实现多个接口,所以就可以继承其他类和实现其他接口。)
2,实现方式更适合处理多线程共享数据的问题。(因为实现方式创建多个线程时,使用的资源都是一份共享的,而使用继承方式创建多个线程时,因为是通过new的方式,所以每个线程使用的资源不是同一份,因此必须使用static变量修饰,以达到资源共享的目的。)
3,线程池只能放入实现Runnable或Callable接口的线程,不能直接放入继承Thread的类。
4,继承Thread:线程代码存放在Thread子类的run()方法中
实现Runnable:线程代码存放在Runnable接口子类的run()方法
1 class Demo implements Runnable{// 1.定义类实现Runnable接口 2 public void run() { //2.重写run方法 3 for(int i=0;i<100;i++){ 4 System.out.println(Thread.currentThread().getName()+"run..."+i); 5 } 6 } 7 public class TreadDemo { 8 public static void main(String[] args) {//main函数也是一个线程 9 Demo d=new Demo(); 10 Thread t1=new Thread(d);//3.通过Thread类建立线程对象 4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数 11 Thread t2=new Thread(d); 12 t1.start();//5.调用Thread类的start方法开启线程并调用Runnable接口子类的run()方法 13 t2.start(); 14 for(int i=0;i<200;i++){ 15 System.out.println("main...."+i); 16 } 17 } 18 }
第三种:实现callable接口<泛型>
步骤:1.创建类(MyCallable)并实现Callable接口。
2.实现call()方法。(同run方法,但不同的是要抛异常)
3.创建FutureTask实例,并将MyCallable类的实例作为参数传递给FutureTask。
4.创建Thread实例,并将FutureTask的实例作为参数传递给Thread。
5.调用Thread实例的start方法来开启线程
6.获取并打印(MyCallable)的执行结果。FutureTask实例.get().
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.FutureTask; 4 5 class MyCallable implements Callable {//1.定义类(MyCallable)去实现Callable接口。 6 @Override 7 public Object call() throws Exception {//2.实现call()方法。 8 for (int i = 0; i <100 ; i++) { 9 System.out.println("子线程"+i); 10 } 11 return "子线程执行完毕"; 12 } 13 } 14 public class MyCallableDemo { 15 public static void main(String[] args) { 16 FutureTask futureTask = new FutureTask(new MyCallable());//FutureTask类实现了Runnable接口 17 // 3.创建FutureTask实例,并创建MyCallable类的实例作为参数传递给FutureTask实例。 18 Thread thread=new Thread(futureTask);//4.创建Tread实例 19 thread.start();//5.调用Tread实例的start方法来开启线程 20 for (int i = 0; i <100 ; i++) { 21 System.out.println("主线程"+i); 22 } 23 try { 24 System.out.println(futureTask.get());//获取子线程的结果 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } catch (ExecutionException e) { 28 e.printStackTrace(); 29 } 30 } 31 }
为什么要创建FutureTask类的实例??
因为Thread接收Runnable类型的,但不接收Callable类型的,所以就需要一个转换器,而FutureTask实现了Runnable接口,且构造方法支持Callable类型的。
FutureTask接口与Runnable接口的关系:
Future接口中常用的方法:
1.判断任务是否完成:isDone()
2.能够中断任务:cance()
3.能够获取任务执行结果:get()
第四种:使用线程池
步骤:1.使用Executor获取线程池对象
2.通过线程池对象获取线程并执行MyRunnable()实例
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 class MyRunnable implements Runnable{//创建MyRunnable()实例
4 5 @Override 6 public void run() { 7 for (int i = 0; i <10; i++) { 8 System.out.println("子线程。。。"+i); 9 } 10 } 11 } 12 public class TreadExtendDemo { 13 public static void main(String[] args) { 14 ExecutorService executorService=Executors.newFixedThreadPool(10);//1.使用Executor获取线程池对象 15 executorService.execute(new MyRunnable());//2.通过线程池对象获取线程并执行MyRunnable()实例
16 for (int i = 0; i <10 ; i++) { 17 System.out.println("main"+i); 18 } 19 } 20 }
第五种:使用匿名对象。
1 public class ThreadDemo { 2 public static void main(String[] args) { 3 Thread thread=new Thread(new Runnable() {//使用匿名方法创建线程 4 @Override 5 public void run() { 6 for (int i = 0; i <100 ; i++) { 7 System.out.println("子线程。。。。"+i); 8 } 9 } 10 }); 11 thread.start();//开启线程 12 13 for (int i = 0; i <100 ; i++) { 14 System.out.println("主线程"+i); 15 } 16 } 17 }
面试题:Runnable和Callable接口比较
相同点:
- 两者都是借口。
- 两者都可用来编写多线程程序。
- 两者都需要调用Tread.start()方法启动线程。
不同点:
- 实现Callable接口的线程能够返回执行结果,而实现Runnable接口的线程不能返回结果;
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的不允许抛异常;
- 实现Callable接口的线程可以调用Future.cancel()取消执行,也可以调用isDone()来判断线程是否执行完,也就是说可以实现对线程的监控,而实现Runnable接口的线程不能;
注意点:Callable接口支持返回执行结果,但需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞。
三,结束线程的方法--(♦使用通知方式)
1.stop()已过时
2.使用Interrupt方法中断线程。
注意点:我一开始看到该方法的时候,认为interrupt会使线程停止运行,但事实上并非如此,调用一个线程的Interrupt方法会把线程的状态改为中断态。这其中又可以细分成两个方面:
1)对于因执行了sleep、wait、join方法而休眠的线程:调用Interrupt方法会使他们不再休眠,同时会抛出 InterruptedException异常。比如一个线程A正在sleep中,这时候另外一个程序里去调用A的interrupt方法,这时就会迫使A停止休眠而抛出InterruptedException异常,从而提前使线程逃离阻塞状态。
2)对于正在运行的线程,即没有阻塞的线程,调用Interrupt方法就只是把线程A的状态改为interruptted,也就是将中断标志改为true,但是不会影响线程A的继续执行。因为一个线程不应该由其他线程来强制中断或自行停止,会造成线程不安全,所以说interrupt()并不能将真正的中断线程,还需要被中断的线程进行配合才行,也就是说一个有中断需求的线程,在执行过程中会不断的检查自己的中断标志位,如果被设置了中断标志,就自行停止线程。
3.采用通知的方法(标志)。
当线程完成执行并结束后,就无法再次运行了。应该通过使用标志来指示run方法退出的方式来停止线程,即通知方式。该方式可通过线程以安全的方式结束运行。
【代码演示】:在main方法中启动两个线程,第一个线程循环随机打印100以内的整数,直到第二个线程从键盘中读取了“over”命令。
1 import java.util.Random; 2 import java.util.Scanner; 3 class TraddDemo_1 implements Runnable{ 4 //第一个线程随机打印100以内的整数 5 boolean flag=true; 6 @Override 7 public void run() { 8 while (flag){ 9 System.out.println(flag); 10 try{ 11 Thread.sleep(3000);//为抢占cpu执行权 12 }catch (Exception e){ } 13 Random in=new Random(); 14 int number=in.nextInt(100)+1;//随机打印1-100之间的数字 15 System.out.println("随机数字:"+number); 16 } 17 } 18 public void setFlag(boolean flag){ 19 this.flag=flag; 20 } 21 } 22 class TreadDemo_2 implements Runnable{//因为两个线程做的事不同,所以要重写一个线程 23 TraddDemo_1 t; 24 public TreadDemo_2(TraddDemo_1 t){ 25 this.t=t;//变量t引用的是TreadDemo_1的地址 26 } 27 @Override 28 public void run() { 29 Scanner in=new Scanner(System.in); 30 while (true){ 31 System.out.println("请输入终止符:"); 32 String str=in.nextLine(); 33 if(str.equals("over")){ 34 t.setFlag(false); 35 break; 36 }else { 37 System.out.println("您输入的终止符不正确!"); 38 } 39 } 40 } 41 } 42 public class ThreadStopDemo { 43 public static void main(String[] args) { 44 TraddDemo_1 traddDemo1=new TraddDemo_1(); 45 TreadDemo_2 treadDemo2=new TreadDemo_2(traddDemo1);//将引用传递给TreadDemo_2 46 new Thread(traddDemo1).start();//创建线程,并启动 47 new Thread(treadDemo2).start(); 48 } 49 }
运行结果:
四,线程的常见方法
yield()方法:暂停当前正在执行的线程对象,会让当前线程由“运行状态”进入到“就绪状态”,并且让步于其他相同或优先级更高的线程执行,如果没有优先级高于当前线程的线程,则当前线程会继续执行。注意不能确定暂停的时间,且即使释放CPU执行权也不会释放锁。
join()方法:若在当前线程中调用B线程的join方法,则当前线程会让B线程插队在自己面前执行,如果B线程抢到了cpu执行权,则B线程肯定会执行完,是一个串行的过程。注B线程有可能没有也没有抢到执行权。
join(long millis)方法:当前线程会让B线程插队在自己面前执行,但只等待millis秒。
join(long millis,int nanos)方法:同join(long millis)方法,因为在底层还是将nanos四舍五入为millis.
注意:调用join会释放锁,因为在join方法里调用了wait方法。
interrupt()方法:中断操作。
1)对于因执行了sleep、wait、join方法而休眠的线程,调用Interrupt方法会使他们不再休眠,同时会抛出 InterruptedException异常。比如一个线程A正在sleep中,这时候在另外一个线程里去调用A的interrupt方法,这时就会迫使A停止休眠从而提前使线程逃离阻塞状态,并且会抛出InterruptedException异常,。
2)对于正在运行的线程,即没有阻塞的线程,调用Interrupt方法就只是把线程A的状态改为interruptted,也就是将中断标志改为true,但是不会影响线程A的继续执行,直到调用了使线程进入阻塞状态的sleep,join,wait方法时才会起到作用,先抛出InterruptedException异常,然后使线程立即跳出阻塞状态。
isinterrupted()方法:判断当前线程是否发生了中断操作,true 已发生中断,false 未发生中断操作。
sleep()方法:线程休眠。哪个线程调用就让哪个线程休眠。
sleep(long millis)
sleep(long millis,int nanos) 都是提供休眠操作。
sleep休眠期间,会让出CPU使用权,但线程仍然持有锁。
sleep休眠时间到了之后,不会立即执行,而是线程由“阻塞状态”进入“就绪状态”。
setDaemon(boolean n)方法:设置守护线程(true)或用户线程(false即默认的)。
用户线程(非守护线程):线程的任务体正常执行完。
后台线程(守护线程):服务于用户线程,所有用户线程执行结束,哪怕守护线程的任务体还没有执行完毕,也会伴随着结束,比如:垃圾回收机制。
守护线程的生命周期:
守护线程的生命周期是依赖于用户线程,当有用户线程存在,守护线程就会存在,当没有用户线程存在,那守护线程也会随之消亡。需要注意的是:Java虚拟机在“用户线程”都结束后是会退出。
Priority优先级:线程的优先级
线程的优先级:就是来指导线程都执行优先级。
方法介绍:
int getPriority() 获取优先级
setPriority() 设置优先级
方法特点:
1.Java线程的优先级并不绝对,它所控制的是执行的机会,也就是说,优先级高的线程执行效率比较大,而优先级低的也并不是没有机会,只是执行的概率相对低一些。
2.Java一共有10个优先级,分别为1-10,数值越大,表明优先级越高,一个普通的线程,其优先级为5;线程的优先级具有继承性,如果一个线程B是在另一个线程A中创建的,则B叫做A的子线程,B的初始优先级与A保持一致。
3.优先级范围:Java中的优先级的范围是1-10。最小值为1,默认的是5,最大值为10。“优先级高的会优先于低的先执行”。
【代码演示】:join方法。
1 class B extends Thread{ 2 @Override 3 public void run() { 4 for (int i = 0; i <4 ; i++) { 5 System.out.println("B正在使用电脑"); 6 } 7 } 8 } 9 public class ThreadJoinDemo { 10 public static void main(String[] args) { 11 B b=new B(); 12 b.start(); 13 for (int i = 0; i <5; i++) { 14 System.out.println("A正在使用电脑"); 15 if(i>=3){ 16 try { 17 b.join();//调用B的join方法后,如果B抢到了执行权则一定会运行完代码。 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } 21 } 22 } 23 } 24 }
运行结果:
【代码演示】:对正在运行的代码调用interrupt方法。
1 public class TestDemo { 2 public static void main(String[] args) { 3 Thread thread=new Thread(new Runnable() { 4 @Override 5 public void run() { 6 int n=0; 7 while (true){ 8 n++; 9 System.out.println("子线程"+n); 10 if(n==100){ 11 System.out.println("休眠"); 12 try { 13 Thread.sleep(100000); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 } 19 20 } 21 }); 22 thread.start(); 23 thread.interrupt(); 24 25 } 26 }
运行结果:
通过结果可知,当线程正在运行这时调用了interrupt方法代码确实没有什么变化,直到调用了sleep方法时,我们可以明显发现线程并没有进入睡眠状态,而是继续运行,并且报出来sleep interrupted异常。
【代码演示】:setDaemon方法
1 class Waiter extends Thread{ 2 @Override 3 public void run() { 4 while(true) {//使用while语句,直到主线程运行结束,该线程才停止。 5 System.out.println("服务员正在工作"); 6 } 7 } 8 } 9 public class TreadSetDaemonDemo { 10 public static void main(String[] args) { 11 Waiter waiter=new Waiter(); 12 waiter.setDaemon(true); 13 waiter.start(); 14 for (int i = 0; i <200 ; i++) { 15 System.out.println("餐厅还在营业!"); 16 } 17 } 18 }
运行结果:
只有当餐厅停止营业,服务员才停止工作。这里之所以后面还打印了3个“服务员正在工作”,是因为打印是单线程的,当餐厅停止营业时,虽然服务员也停止服务了,但是打印里面还缓存了一部分未打印完。
1、 总结wait()和sleep()方法之间的区别和联系
从表面上看,wait()和sleep()都可以使得当前线程进入阻塞状态,但是两者之间存在本质性的差别,下面总结两者的区别和相似之处:
1) wait()和sleep()都可以使得线程进入阻塞状态
2) wait()和sleep()都是可中断方法,被中断之后都会收到中断异常
3) wait()是Object类中的方法,由于wait()调用必须在一个synchronized方法/块中,调用之前需要先获取对象的monitor,每个对象都有自己的monitor,让当前线程等待当前对象的monitor,当然需要当前对象来操作,所以wait()方法就必须要定义在Object类中, 而sleep()是Thread特有的方法
4) wait()方法执行必须在同步方法/同步块中,而sleep()不需要
5) 线程在同步方法中执行sleep()时,并不会释放掉monitor的锁,而wait()会释放掉
6) Sleep()短暂休眠之后会主动退出阻塞,而wait()(没有指定时间)则需要被其他线程中断后/其他线程唤醒并获取到当前对象的monitor后才能退出阻塞
2、 interrupted()和isInterrupted()有什么区别?写一个简单的示例来验证
1) isIntereripted是Thread的一个成员方法,它主要判断当前线程是否被中断,该方法仅仅是对interrupt标识的一个判断,并不影响标识发生任何改变。
2) interrupted是一个静态方法,虽然其也用于判断当前线程是否被中断,但是调用该方法会直接擦除掉线程的interrupt标识,要注意的是,如果当前线程被打断了,那么第一次调用interrupted方法会返回true,并且立即擦除了interrupt标识;第二次包括以后的调用永远都会返回false。
3、 什么是守护线程?为什么会有守护线程?什么时候需要守护线程?
要回答这些问题,我们必须先搞清楚另外一个比较重要的问题:JVM程序在什么情况下会退出?
来自JDK官方文档:The java virtual machine exits when the only threads running are all daemon threads.这句话指的是正常退出的情况,而不是调用了System.exit()方法,通过这句话的描述,我们不难发现,在正常情况下, 若JVM总没有一个非守护线程,则JVM的进程会退出。
如果一个JVM进程中都是守护线程(即没有一个非守护线程存在),那么JVM这一进程在某一程序结束的时候也会退出,也就是说守护线程具备自动结束生命周期的特性,而非守护线程则不具备这一特点。假设JVM进程的垃圾回收线程是非守护线程,那么某一程序完成工作结束,则JVM无法退出,因为垃圾回收线程还在正常的工作。
守护线程经常用做执行一些后台任务,因此有时也被称之为后台线程,当你希望关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够自动关闭,此时就可以考虑使用守护线程为你完成这样的工作。
五.线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。(jvm为线程分配内存,初始化成员变量)
2.就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备运行条件。(jvm为线程创建方法栈和程序计数器,等待线程调度器调度)
3.运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能。
4.阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出cpu,并临时终止自己的执行,进入阻塞状态。
5.死亡:线程完成了它的全部工作或线程被提前强制性的终止。
1、新建和就绪状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
调用线程对象的start()方法之后,该线程立即进入就绪状态——就绪状态相当于"等待执行",但该线程并未真正进入运行状态。如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。
2、运行和阻塞状态
2.1 线程运行状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU。那么在任何时刻只有一个线程处于运行状态,当然在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。
2.2 线程阻塞状态
当发生如下情况时,线程将会进入阻塞状态
① 线程调用sleep()方法主动放弃所占用的处理器资源
② 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将存更深入的介绍
④ 线程在等待某个通知(notify)
⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
2.3 解除阻塞
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
① 调用sleep()方法的线程经过了指定时间。
② 线程调用的阻塞式IO方法已经返回。
③ 线程成功地获得了试图取得的同步监视器。
④ 线程正在等待某个通知时,其他线程发出了个通知。
⑤ 处于挂起状态的线程被调甩了resdme()恢复方法。
3. 死亡状态
线程会以如下3种方式结束,结束后就处于死亡状态:
① run()或call()方法执行完成,线程正常结束。
② 线程抛出一个未捕获的Exception或Error。
③ 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。