Java语言中有一个专门表示线程的Thread类,这个类位于java.lang包下,因此在使用这个类时无需引入。Thread的方法定义了线程的基本操作,下面的表14-1展示了Thread类所定义的方法。
表14-1 Thread类的方法
方法 | 功能 |
String getName() | 获得线程名称 |
void setName(String name) | 设置线程的名称 |
int getPriority() | 获得线程优先级 |
boolean isAlive() | 判定线程是否仍在运行 |
void join() | 等待一个线程运行结束 |
void join(long millis) | 等待一个线程运行结束,等待时间不超过millis毫秒 |
join(long millis, int nanos) | 等待一个线程运行结束,等待时间不超过millis毫秒又nanos纳秒 |
void run() | 执行线程的任务 |
static void sleep(long millis) | 使线程睡眠millis毫秒 |
static void sleep(long millis int nanos) | 使线程睡眠millis毫秒加nanos纳秒 |
static void yield() | 使线程从运行状态转入就绪状态 |
static Thread currentThread() | 获得当前线程对象 |
void start() | 启动线程使之开始执行任务 |
void suspend() | 挂起线程 |
void setDaemon(boolean on) | 当on为true时设置线程为后台线程 |
boolean isDaemon() | 判断线程是否为后台线程 |
Thread类的很多方法都声明了可能抛出的异常,因此在编程时要根据IDE的提示合理的处理这些异常。虽然Java语言定义了Thread类来表示线程,但程序员并不是只能以Thread或其子类来创建线程对象,本小节将讲解线程的多种创建方式。
14.3.1主线程
在14.1小节曾经讲过:每当一个程序开始运行时,系统就会创建一个进程,并且同时在这个进程下创建一个主线程,也就是说:主线程并不是由程序员来完成创建的。主线程是一个很重要的线程,它是产生其它子线程的线程,并且通常它都是最后完成执行,因为一般由主线程执行各种关闭资源的操作。
对于Java程序而言,主线程所要执行的任务都被写在main()方法中。虽然主程序不是由程序员创建的,但程序员可以通过Thread类所定义的currentThread()方法获得主线程的引用。currentThread()方法是Thread类的公有静态方法,它的作用是获得当前线程的引用,也就是说:在哪个线程的任务代码中调用这个方法,就会获得哪个线程的引用。当获得主线程的引用后,程序员可以通过这个引用获得主线程的名称、优先级等各项属性,并且还可以修改这些属性,下面的【例14_01】展示了如何操作主线程。
【例14_01操作主线程】
Exam14_01.java
public class Exam14_01 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("当前线程: " + t);
//更改线程名
t.setName("first thread");
System.out.println("线程的新名字: " + t.getName());
}
}
【例14_01】的程序代码首先通过currentThread()方法获得了线程的引用,由于是在main()方法中调用currentThread()方法,所以获得的必然是主线程的引用。【例14_01】的运行结果如图14-2所示。
图14-2【例14_01】运行结果
图14-2中打印出主线程的信息是“Thread[main,5,main]”,其中中括号中第一个main表示主线程的默认名称,中间的5表示主线程的优先级,第二个main则表示主线程所在的线程组的名称。此外从运行结果图中还可以看到:使用setName()方法可以对主线程重新命名,调用getName()方法能够获得线程的新名称。实际上,任何一个线程都可以被重新命名以及被设置优先级。
14.3.2继承Thread类创建线程
主线程是Java虚拟机所创建的线程,程序员也可以创建自己的线程。创建线程的方法有三种,最常见的方法就是定义Thread类的子类,并且创建一个子类对象作为线程。程序员在定义Thread类时要重写它的run()方法,因为线程被启动后虚拟机就会执行run()方法,因此run()方法中的代码实际上就是线程要执行的任务,重写run()方法就是定义线程要执行的任务。
当创建出子类对象后,不能直接调用run()方法让这个线程执行任务,必须调用start()方法启动线程。线程启动后,会自动在start()方法中调用run()方法去执行线程的任务。如果程序员直接调用run()方法,那么虚拟机会把Thread子类对象当作一个普通对象而不是一个线程来看待,更不会以调度线程的方式对其进行调度,并且还会把run()方法当作一个普通方法,因此程序又变成了单线程模式。当子线程被启动后,它的地位与主线程相同,各自执行自己的任务,轮流使用CPU。实际上,主线程与子线程对CPU是一种“争夺使用”的方式,并不是完全按照“交替”的方式使用。下面的【例14_02】展示了如何创建子线程以及子线程与主线程争夺使用CPU的效果。
【例14_02继承Thread类创建线程】
Exam14_02.java
class NewThread extends Thread{//创建Thread类的子类
@Override
public void run(){
for(int i=1;i<=5;i++){
try {
System.out.println("子线程:"+i);
Thread.sleep(500);//让线程睡眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Exam14_02 {
public static void main(String[] args) {
NewThread nt = new NewThread();
nt.start();
for(int i=1;i<=5;i++){
try {
System.out.println("主线程:"+i);
Thread.sleep(500);//让线程睡眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
【例14_02】的Exam14_02.java文件中定义了两个类,其中NewThread继承了Thread类,因此这个类也是一个线程,它的run()方法以循环的形式打印了1~5这5个整数,并且每次打印完之后都调用sleep()方法让线程睡眠500毫秒,由于sleep()方法声明了可能抛出InterruptedException,所以在调用这个方法时要用try-catch结构对异常进行处理。Exam14_02类的main()方法中的代码实际上是主线程的任务,这个任务包括两部分:创建并启动子线程以及循环打印5个整数,每次打印完整数也让主线程睡眠500毫秒。之所以要让主线程和子线程都进行睡眠,是为了不让某个线程“一口气”执行完毕,从而使两个线程能够轮流执行。由于线程对CPU的使用方式是“相互争夺”,这导致两个线程在同时结束睡眠后,哪一个线程能够获得CPU具有了一定的随机性,所以【例14_02】的运行结果并不是固定不变的,下面的图14-3展示了【例14_02】的两个不同的运行结果。
图14-3【例14_02】运行结果
图14-3以①和②标记了【例14_02】的两次运行结果,可以看到:在②这一部分中,子线程在结束睡眠后曾经连续两次争夺到了CPU的使用权(图中方框部分),这充分证明线程之间并不是完全按照“交替”的方式使用CPU的。
14.3.3实现Runnable接口创建线程
程序员除了可以用继承Thread类的方式创建线程,还可以用实现Runnable接口的方式创建线程,具体做法是:
- 定义一个Runnable接口的实现类,并且实现Runnable接口所定义的run()抽象方法。实现run()抽象方法实际上也是定义线程要执行的任务。
- 创建一个实现类的对象,为方便讲述,此处称实现类的对象为target。
- 再创建一个Thread类对象,创建Thread类对象时,要以target作为构造方法的参数。
实际上,第3步所创建的这个Thread类对象才是真正的线程,这个线程要执行的任务是target对象run()方法中的代码。下面的【例14_03】展示了实现Runnable接口创建线程的过程。
【例14_03实现Runnable接口创建线程】
Exam14_03.java
class RunThread implements Runnable{//定义Runnable接口的实现类
String name;//对象的名字
public RunThread(String name){
this.name = name;
}
@Override
public void run() {//实现run()方法
for(int i=1;i<=5;i++){
try {
System.out.println("线程"+name+":"+i);
Thread.sleep(500);//让线程睡眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Exam14_03 {
public static void main(String[] args) {
RunThread r1 = new RunThread("A");
RunThread r2 = new RunThread("B");
new Thread(r1).start();//创建线程并启动
new Thread(r2).start();//创建线程并启动
}
}
【例14_03】的代码中,RunThread是一个实现了Runnable接口的类,这个类的两个对象r1和r2被当作了Thread类构造方法的参数,这样,当启动由Thread类对象创建的线程后,线程就会执行RunThread类中run()方法中的代码。由于线程的执行具有一定的随机性,因此下面的图14-4展示了【例14_03】的两组运行结果。
图14-4【例14_03】运行结果
图中方框中的部分是一个线程连续两次抢到CPU的情况。
14.3.4实现Callable接口创建线程
采用继承Thread类和实现Runnable接口创建线程时,都要编写run()方法中的代码来定义线程所要完成的任务,但run()方法没有返回值,因此如果需要线程计算某个数学公式时run()方法不能直接返回计算结果,这样看来run()方法具有一定的局限性。为解决这个问题,从JDK1.5开始,Java语言引入了Callable接口来解决这个问题。Callable接口中定义了一个call()抽象方法,这个方法相当于run()方法,它也能定义线程所要完成的任务,但call()方法具有返回值,这样就很好的弥补了run()方法没有返回值的弱点。Callable是一个泛型接口,因此在定义它的实现类时要指定类型参数,这个类型参数实际上代表了call()方法的返回值类型。
使用Callable接口创建线程的过程稍微有点复杂,因为整个过程中不仅需要用到Callable接口,还需要用到FutureTask类。Callable接口和FutureTask类都位于java.util.concurrent包下,所以在使用它们时需要用import关键字进行引入。使用Callable接口创建线程的过程主要分为以下几个步骤:
- 定义一个Callable接口的实现类,在定义实现类时指定类型参数。
- 在实现类中实现Callable接口中的call()方法。
- 创建一个FutureTask类对象,并且在创建对象时以Callable接口的实现类作为构造方法的参数。
- 以创建好的FutureTask类对象作为构造方法的参数创建一个Thread类对象,这个对象就是线程。
从以上步骤可以看出:最终创建出的Thread类对象中包装了一个FutureTask类对象,而FutureTask类对象中又包装了一个Callable接口的实现类对象。线程要执行的任务实际上就是Callable接口的实现类call()方法中的代码。由于call()方法不是被程序员调用的,所以程序员如果想获得call()方法的返回值,需要调用FutureTask类的get()方法实现,get()方法的返回值实际上就是call()方法的返回值,下面的【例14_04】展示了使用实现Callable接口的方式创建线程的过程。
【例14_04实现Callable接口创建线程】
Exam14_04.java
import java.util.concurrent.*;
//定义Callable接口的实现类并指定类型参数
class CallThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {//①实现call()方法
int sum = 0;
for (int i=1;i<=5;i++){
System.out.println("子线程:"+i);
sum = sum+i;//累加
Thread.sleep(500);
}
return sum;
}
}
public class Exam14_04 {
public static void main(String[] args) {
try {
//创建FutureTask类对象
FutureTask<Integer> task = new FutureTask<Integer>(new CallThread());//②
//以FutureTask类对象作为Thread构造方法的参数创建并启动线程
new Thread(task).start();
for (int i=1;i<=5;i++){
System.out.println("主线程:"+i);
Thread.sleep(500);
}
System.out.println("call()方法的返回值是:"+task.get());//③
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
为方便读者阅读,【例14_04】的代码以注释的形式标注出了创建线程的4个步骤。需要注意:因为在定义Callable接口的实现类CallThread时指定了类型参数的具体类型为Integer,因此语句①在实现call()方法时也要把返回值类型定义为Integer。FutureTask也是一个泛型类,创建对象时要把类型参数设置为与Callable接口的实现类一致,否则会出现语法错误,本例的语句②按照这个规定把类型参数也指定为Integer。
【例14_04】中CallThread类的call()方法以循环的形式打印了变量i的值,并且在循环中通过累加的方式计算了所有变量i之和,最后把累加之和作为方法的返回值。而main()方法也是使用循环的方式打印了变量i的值,这样程序中就有主、子两个线程同时运行。语句③中,调用task对象的get()放回获得了call()方法的返回值。【例14_04】的运行结果如图14-5所示。
图14-5【例14_04】运行结果
14.3.5创建线程的各种方式特点对比
14.3小节主要讲解了线程的创建,其中主线程是虚拟机创建的,因此不做讨论。而子线程的创建又分为3种方式。这3种方式中,通过继承Thread类或实现Runnable、Callable接口都可以创建线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现 Callable接口归为一种方式。
采用实现Runnable、Callable接口的方式创建多线程的优点是:首先,线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。其次,在这种方式下多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源。而这种创建方式的缺点是:编程稍显复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建线程的优点是:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this 即可获得当前线程。而这种创建方式的缺点是:因为线程类已经继承了Thread类,所以不能再继承其他父类。
由于实际开发过程中线程有可能要作为其他类的子类,因此一般推荐采用实现Runnable、Callable接口的方式来创建多线程。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。
标签:14,Thread,创建,接口,线程,第十四章,多线程,方法 From: https://blog.51cto.com/mugexuetang/5983889