首页 > 其他分享 > 第十四章《多线程》第3节:创建线程

第十四章《多线程》第3节:创建线程

时间:2023-01-02 17:31:51浏览次数:56  
标签:14 Thread 创建 接口 线程 第十四章 多线程 方法

​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所示。​

 第十四章《多线程》第3节:创建线程_主线程

图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】的两个不同的运行结果。​

 第十四章《多线程》第3节:创建线程_多线程_02

图14-3【例14_02】运行结果​

图14-3以①和②标记了【例14_02】的两次运行结果,可以看到:在②这一部分中,子线程在结束睡眠后曾经连续两次争夺到了CPU的使用权(图中方框部分),这充分证明线程之间并不是完全按照“交替”的方式使用CPU的。​

14.3.3实现Runnable接口创建线程

程序员除了可以用继承Thread类的方式创建线程,还可以用实现Runnable接口的方式创建线程,具体做法是:​

  1. 定义一个Runnable接口的实现类,并且实现Runnable接口所定义的run()抽象方法。实现run()抽象方法实际上也是定义线程要执行的任务。​
  2. 创建一个实现类的对象,为方便讲述,此处称实现类的对象为target。​
  3. 再创建一个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】的两组运行结果。​

 第十四章《多线程》第3节:创建线程_Java_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接口创建线程的过程主要分为以下几个步骤:​

  1. 定义一个Callable接口的实现类,在定义实现类时指定类型参数。​
  2. 在实现类中实现Callable接口中的call()方法。​
  3. 创建一个FutureTask类对象,并且在创建对象时以Callable接口的实现类作为构造方法的参数。​
  4. 以创建好的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所示。​

 第十四章《多线程》第3节:创建线程_多线程_04

图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

相关文章

  • java多线程
    1、线程:进程想要执行任务就需要依赖线程。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。 线程不能独立存在,必须依赖进程,在进程中运行每一个进......
  • 静态方法如何调用非静态方法,创建线程的方式
    在静态方法中,new一个类对象,用类对象调用非静态方法第一种方式创建线程publicclassMain{  privateclassMyRunimplementsRunnable{    publicvoid......
  • C#开发的线程池和管理器 - 开源研究系列文章
          上次编写了一个小软件,用于练手及自己的一个小工具集合。今天把其中的线程池和管理器的代码抽取出来,写成一个博文,让需要的朋友能够进行学习和应用。    ......
  • 1.走近Java世界中的线程
    一.基本概念 进程是程序运行的实例。 进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位。线程是进程中可独立执行的最小单位。 一个进程中可以包含多个......
  • 04.关于线程你必须知道的8个问题(下)
    今天我们来学习线程中最后4个问题:线程的同步与互斥线程的本质与调度死锁的产生与解决多线程的是与非通过本篇文章,你可以了解到计算机中经典的同步机制--管程,Java线......
  • Python之路【第七篇】:线程、进程和协程
    1.Python线程Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。#!/usr/bin/envpython#-*-coding:utf-8-*-importthreadingimporttime......
  • 网络程序设计 实验3 多人聊天室 流式套接字 多线程编程
    实验3多人聊天室实验目的:通过流式套接字编程,及多线程编程,实现简单的多人聊天室。开发语言与工具:VC实验要求:1.使用MFC编程。2.利用流式套接字编程及多线程编程。3......
  • Linux线程控制
    写在前面我们今天来看线程的知识点,这个博客的内容很多,主要就是为了我们后面的网络做铺垫,最关键的是相比较于进程而言,线程是更加优秀的,我们现在的计算机大多采用的就是线程.......
  • Linux线程互斥
    写在前面这个博客的内容很少,但是很关键,这是我们线程安全相关的内容,里面会涉及到线程互斥和加锁的相关观念,总体而言还是很难的.线程互斥先看一下下面的代码,这里是一切的......
  • Linux 多线程原理深剖
    目录​​传统艺能......