线程创建
线程的创建有三种方法:继承Thread、实现Runnable接口、使用Callable和Future.
1、继承Thread类创建线程类
步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动线程。
public class FirstThread extends Thread{//继承Thread类private int i;public void run() {for(;i<100;i++)System.out.println(getName()+" "+i);}public static void main(String[] args){new FirstThread().start();//创建并启动第一个线程new FirstThread().start();//创建并启动第二个线程}
}
虽然上面程序只显式地创建了两个线程,但实际上程序有三个线程,即两个子线程和一个主线程。当Java运行时,程序至少创建一个主线程,该主线程的执行体不是由run()方法确定的,而是由main()方法确定。
程序可以通过setName(String name)为线程设置名字,也可以通过getName()方法返回线程的名字。在默认情况下,主线程名字为main,其他线程名字依次为Thread-0、Thread-1、...、Thread-n。
currentThread()是Thread()类的静态方法,该方法返回当前正在执行的线程对象。
2、实现Runnable接口创建线程类
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用start()方法来启动线程。
注:Java 8 增加了lambda(λ)表达式,因为Runnable接口是函数式接口,所以可以直接使用lambda表达式来创建Runnable对象。
public class SecondThread implements Runnable{private int i;@Overridepublic void run() {// TODO Auto-generated method stubfor(;i<100;i++)System.out.println(Thread.currentThread()+" "+i);}public static void main(String[] args){SecondThread st = new SecondThread();new Thread(st,"新线程1").start();new Thread(st,"新线程2").start();}
}
Runnable对象仅仅作为Thread对象的target,Runnable实现类中的run()方法仅仅作为线程执行体。而世纪的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。正因为程序所创建的Runnable对象只是线程的target, 而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。
3、使用Callable和Future创建线程类
上面已经指出,通过Runnable接口创建线程时,Thread类的作用是把run()方法包装成线程执行体。那么可不可以直接把任意方法包装成线程执行体呢?Java目前不行。(C#可以)
但是从Java 5开始,Java提供了Callable接口,该接口提供一个call()方法作为线程执行体,并且可以有返回值,还可以声明抛出异常。这很像Runnable接口的增强版,因此可以想出使用Callable对象作为Thread的target。问题是:Callable接口不是Runnable的自接口,因此不能直接作为Thread的target。
Java 5同时提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Callable接口和Runnable接口,可以作为Thread的target使用。
步骤如下:
- 创建Callable接口的实现类,实现call()方法,再创建Callable实现类的实例。(Callable接口也是函数式接口,可以使用lambda表达式)
- 使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target,创建并启动线程。
- 调用FutureTask对象的gat方法来获得子线程执行结束后的返回值。
public class ThirdThread {public static void main(String[] args) {ThirdThread rt = new ThirdThread();FutureTask<Integer> task = new FutureTask<Integer> (Callable<Integer>)()->{//lambda表达式int i = 0;for(;i<100;i++)System.out.println(Thread.currentThread()+" "+i);});new Thread(task,"有返回值的线程").start();try {System.out.print("子线程返回的值"+task.get());}catch(Exception ex) {ex.printStackTrace();}}
}
Future接口定义了如下几个共有方法:
- boolean cancel(boolean mayInterruptIfRunning):试图取消Future里关联的Callable。
- V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子程序结束后才能得到返回值。
- V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果超出时间Callable任务依然没有返回值,抛出TimeoutException异常。
- boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。
- boolean isDone():如果Callable任务已完成,则返回true。
创建线程的三种方式对比:
采用Runnable、Callable接口实现多线程:
- 线程类只是实现了接口,还可以继承其他类;
- 多个线程可以共享同一个target对象,非常适合多个相同线程来处理同一份资源的情况;
- 劣势是,变成稍微复杂,如果想访问当前线程,需要使用Thread.currentThread()方法。
采用继承Thread类实现:
- 劣势是,因为继承了Thread类,无法继承其他类;
- 优势是,编程简单。
综上,一般推荐使用实现接口的方式来创建多线程。
五态模型
五态模型:在线程的生命周期中,有五种状态,分别是新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。
新建和就绪态:
当程序使用new关键字创建一个线程后,该线程就处于新建状态;当调用start()方法后,该线程就处于就绪态。
启用线程使用start()方法,不能使用run()方法!如果直接调用run()方法,系统把线程对象当作普通对象,run()方法当作普通方法而不是线程执行体。
只能对处于就绪态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
如果希望调用子程序的start()方法后子线程立即执行,可以使用Thread.sleep(1)让当前运行的线程睡眠1毫秒。因为这一毫秒CPU不会空闲,它会去执行另一个就绪的线程。
运行态和阻塞态:
处于就绪态的线程获得CPU进入运行态。但一个线程一般不会一直处于运行态,当发生下面的情况时,线程将进入阻塞态:
· 线程调用sleep()方法主动放弃所占用的处理器资源。
· 线程调用一个阻塞式IO方法,在该方法返回前该线程被阻塞。
· 线程试图获得一个同步监视器,但该监视器正被其他线程所持有。
· 线程在等待某个通知(notify)。
· 程序调用了线程的suspend()方法将线程挂起。但这个方法容易导致死锁,不建议使用。
针对上面的几种情况,当发生一下情况时线程会解除阻塞态重新进入就绪态:
· 调用sleep()方法经过了指定时间。
· 线程调用的阻塞式IO已经返回。
· 线程成功地获取了试图取得的同步监视器。
· 线程正在等待某个通知时,其他线程发出了通知。
· 处于挂起的线程被调用了resume()恢复方法。
注意:线程从阻塞态只能进入就绪态,不能直接进入运行态。调用yield()方法可以让运行态的线程进入就绪态。
线程死亡:
线程会以下面三种方式结束,进入死亡状态:
· run()或call()方法执行完成,线程正常结束。
· 线程抛出一个未捕获的Exception或Error。
· 直接调用stop()方法结束线程----该方法容易导致死锁,不建议使用。
注意:
当主线程死亡时,其他线程不受影响,并不会随之结束。一旦子线程启动后,它就和主线程有着相同的地位,不受主线程影响。
可以用isAlive()方法测试一个线程是否死亡。当线程处于就绪、运行、阻塞时返回true,处于新建、死亡时,返回false。
不要对处于死亡状态的线程调用start()方法,对新建状态的线程调用两次start()方法也是错误的。都会引发IllegalThreadStateException异常。
控制线程
join线程:
Thread提供了一个让一个线程等待另一个线程完成的方法----join()方法。当某个程序执行流中调用其他线程的join()方法时,调用线程就会阻塞,直到被join线程执行完毕为止。
join()方法有以下三种重载形式:
1. join():等待被join线程执行完成。
2. join(long millis):等待被join线程的时间最长为millis毫秒,超出时间则不再等待。
3. join(long millis, int nanos):等待被join线程最长为millis毫秒加nanos毫微秒。一般不使用该形式,一则程序对时间的精度无需精确到毫微秒;二则计算机硬件、操作系统无法精确到毫微秒。
后台线程:
这种线程在后台运行,为其他线程提供服务。也叫“守护线程”、“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有前台线程死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将线程指定为后台线程,注意,该设置必须在线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。判断一个线程是否为后台线程用Thread类的isDaemon()。
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间并进入阻塞状态,可以调用Thread对象的sleep()方法来实现。sleep()方法有两种重载方式。
· static void sleep(long millis): 让当前正在执行的线程暂停millis毫秒并进入阻塞状态。
· static void sleep(long millis,int nanos): 让当前正在执行的线程暂停millis毫秒加nanos毫微秒并进入阻塞状态。同样的原因不建议使用第二种形式。
当一个线程调用sleep()进入阻塞状态后,在其睡眠时间内不会获得执行机会,即使当前系统中没有其他可执行线程。因此sleep()常用来暂停程序的执行。
线程让步:yield
yield()和sleep()有点类似,它也可以让当前正在执行的线程暂停,但它不会阻塞线程,只是将该线程转入就绪态。yield()只是让线程暂停一下,让系统重新调度一下。完全有可能一个线程调用yield()后又立即被调度出来执行。
sleep()和yield()的区别:
· sleep()方法暂停当前线程后会给其他线程机会,不理会其他线程的优先级;yield()只会给优先级相同或更高的线程机会。
· sleep()方法会将线程转入阻塞态,而yield()方法不会阻塞线程。
· sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法要么捕捉异常要么显式声明抛出异常;而yield()方法没有声明抛出任何异常。
· sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法控制并发线程的执行。
改变线程优先级:
每个线程默认的优先级都和它的父线程相同。一般情况下main()具有一般优先级,由它创建的子线程也具有一般优先级。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和获取优先级。setPriority()方法参数是一个1~10的整数,也可以使用Thread类的三个静态常量:
· MAX_PRIORITY: 其值为10;
· MIN_PRIORITY: 其值为1;
· NORM_PRIORITY: 其值为5;
因为有些操作系统不支持10个这么多的优先级,所以为了程序的兼容性最好使用三个静态常量。