Java 高并发核心编程 ----- 初识多线程(上)

文章目录

    • [0. 读前小故事(摘抄自我最喜爱的博主之一---小林Coding)](#0. 读前小故事(摘抄自我最喜爱的博主之一---小林Coding))
    • [1. 线程与进程](#1. 线程与进程)
        • [1.1 什么是进程](#1.1 什么是进程)
        • [1.2 什么是线程](#1.2 什么是线程)
        • [1.3 线程与进程的区别](#1.3 线程与进程的区别)
    • [2. 创建线程的 4 种方法](#2. 创建线程的 4 种方法)
        • [2.1 Thread类详解](#2.1 Thread类详解)
        • [2.2 线程创建方法一:继承 Thread 类创建线程类](#2.2 线程创建方法一:继承 Thread 类创建线程类)
        • [2.3 线程创建方法一:实现 Runnable 接口创建线程类](#2.3 线程创建方法一:实现 Runnable 接口创建线程类)
        • [2.4 优雅创建 Runnable 线程目标类的 2 种方式](#2.4 优雅创建 Runnable 线程目标类的 2 种方式)
        • [2.5 线程创建方法三:使用 Callable 和 FutureTask 创建线程](#2.5 线程创建方法三:使用 Callable 和 FutureTask 创建线程)
        • [2.6 线程创建方法四:通过线程池创建线程](#2.6 线程创建方法四:通过线程池创建线程)

特此注明 :
Designed By :长安城没有风
Version:1.0
Time:2026.1.17
Location:四川 · 成都

本文为读者阅读《Java 高并发核心编程 卷2》(作者:尼恩)后摘抄部分段落以及整合个人理解后重写书写,推荐感兴趣的朋友可以阅读一下原著,如果有侵权可以私信作者进行删除。

0. 读前小故事(摘抄自我最喜爱的博主之一---小林Coding)

我们写好的一行行代码,为了让其工作起来,我们还得把它送进城(进程)里,那既然进了城里,那肯定不能胡作非为了。

城里人有城里人的规矩,城中有个专门管辖你们的城管(操作系统),人家让你休息就休息,让你工作就工作,毕竟摊位不多,每个人都要占这个摊位来工作,城里要工作的人多着去了。

所以城管为了公平起见,它使用一种策略(调度)方式,给每个人一个固定的工作时间(时间片),时间到了就会通知你去休息而换另外一个人上场工作。

另外,在休息时候你也不能偷懒,要记住工作到哪了,不然下次到你工作了,你忘记工作到哪了,那还怎么继续?

有的人,可能还进入了县城(线程)工作,这里相对轻松一些,在休息的时候,要记住的东西相对较少,而且还能共享城里的资源。

1. 线程与进程

从1946年2月14日世界上第一台计算机在宾夕法尼亚大学诞生到今天,计算和处理的模式早已从单用户单任务的串行处理模式发展到了多用户多任务的高并发处理模式。

计算机处理任务的调度单位就是今天我们所讲的进程和线程。我们可以在Windows系统上打开任务管理器,这样我们就会看到一张系统进程的列表,列出了当前系统中运行的所有进程。列表的每一行展示出了进程的详细信息,以及所占用的系统资源,包括CPU,内存,磁盘等。

在Windows操作系统中,进程被分为后台进程和应用进程两类。大部分后台进程在系统开始运行时被操作系统启动,完成操作系统的基础服务功能。应用进程主要由用户启动,完成用户所需要的具体功能,比如听音乐,社交聊天,浏览网站等。

简单来说,进程就是程序的一次执行。什么是程序的?程序是存放在硬盘上的可执行文件,主要包含代码指令和数据。

1.1 什么是进程

在计算机中,CPU是核心的硬件资源,承担了所有的计算任务;内存资源承担了运行时数据的保存任务;外存资源(硬盘等)承担了数据外部永久存储的任务。

计算任务的调度,资源的分配由操作系统来决定。应用程序以进程的形式运行于操作系统系统之上,享受操作系统提供的服务。

进程的定义一直以来没有完美的标准。一般来说,一个进程由程序段,数据段和程序控制块(Program Control Block)三部分组成。

程序段一般也被称为代码段,是进程的程序指令在内存中的位置,包含需要执行的指令集合。

数据段是进程的操作数据在内存中的位置,包含需要操作的数据集合。

程序控制块包含进程的描述信息和控制信息,是进程的唯一标志,由以下部分组成:

  1. 进程的描述信息 。主要包括:进程名称和进程ID,进程ID是唯一的,代表进程的身份;进程的状态,比如运行,阻塞,就绪;进程的优先级,是进程调度的重要依据。
  2. 进程的调度信息。主要包括:程序起始地址,程序的第一行指令的内存地址,从这里开始执行程序的执行。
  3. 进程的资源信息。主要包括:内存信息,内存占用情况和内存管理所用的数据结构;I/O设备信息,所用的I/O设备编号以及相应的数据结构;文件句柄,所打开的文件信息。
  4. 进程上下文。主要包括执行时各种CPU寄存器的值,当前程序计数器的值以及各种栈的值,即进程的环境。在操作系统切换进程时,当前线程被迫让出CPU,当前进程的上下文就存在该进程的PCB中,以便下次恢复的时候使用。

现在操作系统中,进程是并发执行的,任何进程都可以和其他进程一起执行。在进程内部,代码段和数据段有自己的独立地址空间,不同进程的地址空间是隔离的。

作为Java工程师来说,这里有一个问题,什么是Java程序的进程呢?

Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。在这个JVM进程内部,所有的Java代码都是以线程来运行的。JVM找到程序的入口 main() 方法,然后运行 main() 方法,这样就产生一个进程,这个进程称为主进程,当 main() 方法结束后,主线程运行结束,JVM进程也随之退出。

1.2 什么是线程

早期的操作系统只有进程没有线程。进程是程序执行和系统进行并发调度的最小单位。随着计算机的发展,CPU的性能越来越高,从早期的20MHz发展到了现在2GHz以上,从单核CPU到现在的多核CPU,性能提升了成千上万倍。为了充分发挥CPU的计算性能,提升CPU的硬件资源的利用率,同时祢补进程调度过于笨重产生的问题,进程内部演化出了并发调度的诉求,于是就发明出了线程。

线程是指 "进程代码段" 的一次顺序执行流程。线程是CPU调度的最小单位。一个进程可以有一个或者多个线程,各个线程之间共享进程的内存空间,系统资源,进程仍然是操作系统分配资源的最小单位。

Java程序的进程执行过程中就是标准的多线程的执行过程。每当使用Java命令执行一个类时,实际上就是启动了一个JVM进程。理论上,在该进程的内部至少会启动两个线程,一个main线程,一个GC(垃圾回收)线程。

一个标准的线程由三部分组成,即线程描述信息,程序计数器,和栈内存。

在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段的内存地址。

在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程独立所有,在线程之间不共享,也不垃圾管理器管理。

在线程的结构中,线程的描述信息即线程的基本信息,主要包括:

  1. 线程名称。主要是方便用户识别,用户可以指定线程的名称,如果没有指定,系统会自动分配一个名称。
  2. 线程ID。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
  3. 线程优先级。表示线程调度的优先级,优先级越高,获得CPU执行的机会就越大。
  4. 线程状态。表示当前线程的执行状态,为新建,就绪,准备,阻塞,运行,结束等状态的一种。
  5. 其他。例如是否为守护线程等,后面会详细介绍。

下面是一段简单的演示代码,演示一个Java程序的线程信息。

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        System.out.println("Thread name: " + Thread.currentThread().getName());
        System.out.println("Thread priority: " + Thread.currentThread().getPriority());
        System.out.println("Thread state: " + Thread.currentThread().getState());
        System.out.println("Thread ID: " + Thread.currentThread().getId());
    }
}

//Thread name: main
//Thread priority: 5
//Thread state: RUNNABLE
//Thread ID: 1
1.3 线程与进程的区别

下面总结一下进程与线程的区别,主要有以下几点:

  1. 线程是 "进程代码段" 的一次的顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。
  2. 线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发程度很高。
  3. 线程是出于高并发的调度诉求从进程内部演化而来的,进程的出现即充分发挥CPU的计算性能,又祢补了进程调度过于笨重的问题。
  4. 进程之间是相互独立的,但进程内部各个线程之间并不完全独立。各个线程之间共享进程的方法区内存,堆内存,系统资源(文件句柄,系统信号等)。
  5. 切换速度不同,线程上下文切换速度比进程上下文切换要快得多。所以,有时线程也被称为轻量级进程

2. 创建线程的 4 种方法

Java进程中每一个线程都对应着一个Thread实例。线程的描述信息在Thread实例属性中得到保存,供JVM进行线程管理和调度时使用。

Thread类除定义了很多操作线程实例的成员方法外,还有一系列的静态方法。比如 Thread.currentThread() 方法,该方法的作用是取得当前在CPU内核上正在运行的线程实例。

说明:虽然一个进程有很多个线程,但是在一个CPU内核上,同一时刻只能有一个线程是正在执行的,该线程也叫当前线程。

Thread类是Java多线程的基础。

2.1 Thread类详解

Thread类位于 java.lang 包下,有许多重要的属性和方法,用于存储和操作线程的描述信息,接下来为大家逐一介绍Thread类中比较重要的属性和方法。

1. 线程ID

属性:private int tid,此属性用来保存线程的ID。这是一个 private 类型属性,外部只可以通过 getID() 方法进行访问线程ID。

方法:public long getID() ,获取线程ID,线程ID由JVM负责管理,在进程内唯一。

2. 线程名称

属性:private String name,该属性保存一个Thread线程实例的名称。

方法一:public final String getName() ,获取线程名称。

方法二:public final synchronized void setName(String name) ,设置线程名称。

方法三:public Thread(String name) ,通过此构造方法给线程设置一个定制化的名字。

3. 线程优先级

属性:private int priority,该属性保存一个Thread线程实例的优先级。

方法一:public final int getPriority() ,获取线程优先级。

方法二:public final int setPriority() ,设置线程优先级。

Java线程优先级的最大值是10,最小值是1,默认值是5,这三个优先级为三个常量值,也是在Thread类中使用类常量定义的。

4. 是否为守护线程

属性:private boolean daemon = false,该属性保存Thread线程实例的守护状态,默认为 false ,表示是用户的普通线程,不是守护线程。

方法:public final void setDaemon(boolean on) , 将线程实例标记为守护线程或用户线程,如果参数值为 true ,那么将线程实例标记为守护线程。

5. 线程的状态

属性:private volatile int threadStatus,该属性以整数形式保存线程的状态。

方法:public State getState() , 返回当前线程的执行状态,为新建,就绪,运行,阻塞,结束中的一种。

Thread的内部静态枚举类 State 用于定义Java线程的所有状态,具体如下:

java 复制代码
public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

在Java线程的状态中,就绪状态和运行状态在内部都用相同的 RUNNABLE 表示。就绪状态表示该线程具备运行条件,正在等待获取CPU时间片;运行状态表示线程已经获取到了CPU时间片,CPU正在执行先策划概念代码逻辑。

6. 线程的启动和运行

方法一:public void start() ,用来启动一个线程,当调用 start() 方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配资源。

方法二:public void run() ,作为线程代码逻辑的入口方法。run() 方法不是用户程序来调用的,当调用 start() 方法启动一个线程之后,只要线程获得了CPU执行时间,便进入 run() 方法去执行具体的用户线程代码。

7. 获得当前线程

方法:public static native Thread currentThread() ,该方法是一个非常重要的静态方法,用于获取当前线程的Thread实例对象。

2.2 线程创建方法一:继承 Thread 类创建线程类

通过前面的讲解我们可以知道,新线程如果要并发执行自己的代码,需要做以下两件事情:

  1. 需要继承 Thread 类,创建一个新的线程类。
  2. 同时重写 run() 方法,讲需要并发执行的业务代码编写在 run() 方法中。

下面的示例演示如何通过继承Thread类创建一个线程类,新的线程子类重写了Thread的 run() 方法,实现了用户业务代码的并发执行,具体如下:

java 复制代码
public class ThreadDemo extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
            }
        }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
        }
    }
}
2.3 线程创建方法一:实现 Runnable 接口创建线程类

通过继承 Thread 类并重写 run() 方法只是创建Java线程的一种方式。是否可以不继承 Thread 类实现线程新建呢?答案是肯定的。

让我们看一下 Thread 类中 run() 方法的源码,里面其实有点玄机,代码如下:

java 复制代码
public class Thread implements Runnable {

	private Runnable target;
	
	@Override
	public void run() {
		if (target != null) {
			target.run();
		}
	}
    
}

在Thread 类 run() 方法中,如果 target 不为空,就执行 target 中的 run() 方法,而 target 是 Thread 类的一个实例属性,并且 target 属性的类型是 Runnable 。那什么情况下 target 属性非空呢?Thread 类有一系列的构造器,其中有多个构造期可以为 target 属性赋值,这样的构造器包括如下两个:

  1. public Thread(Runnable target)
  2. public Thread(Runnable target, String name)

使用这两个构造期传入 target 执行目标实例,就可以直接通过Thread 类中 run() 方法以默认方式实现,达到并发编程的目的。在这种场景下,可以不通过继承 Thread 类来实现线程类的创建。在为 Thread 的构造器传入 target 之前,我们先来看看 Runnable 接口是何方神圣。

1. Runnable 接口

Runnable 接口是一个极为简单的接口,位于 java.lang 包下。接口中只有一个方法 run(),具体的源代码如下:

java 复制代码
package java.lang;
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 有且仅有一个抽象方法 ----- run() ,代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在 Runnable 实现类的 run() 中。当 Runnable 实例传入 Thread 实例的 target 属性后,Runnable 接口的 run() 方法将被异步调用。

2. 通过实现 Runnable 接口创建线程类

创建线程的第二种方法就是实现 Runnable 接口,将需要异步执行的业务逻辑代码放在 Runnable 实现类的 run() 方法中,将 Runnable 实例作为 target 执行目标传入 Thread 实例。该方法的具体步骤如下:

  1. 定义一个新类实现 Runnable 接口。
  2. 实现 Runnable 接口中的 run() 抽象方法,将线程代码逻辑存放在该 run() 方法实现版本中。
  3. 通过 Thread 类创建线程对象,将 Runnable 实现类实例作为实际参数传递给 Thread 类的构造器,由 Thread 构造器将该 Runnable 实例赋值给自己的 target 目标执行属性。
  4. 调用 Thread 实例的 start() 方法启动线程。
  5. 线程启动后,线程的run() 方法将被 JVM 执行,该 run() 方法将调用到 target 属性的 run() 方法,从而完成 Runnable 实现类中业务代码逻辑的并发执行。

按照上面的5步,即可实现一个简单的并发执行的多线程演示实例,代码如下:

java 复制代码
public class ThreadDemo implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
            }
        }

    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadDemo());
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
        }
    }
}
2.4 优雅创建 Runnable 线程目标类的 2 种方式

1. 通过匿名类优雅创建 Runnable 线程目标类

在实现 Runnable 接口编写 target 执行目标类时,如果 target 实现类是一次性类,可以使用匿名实例的形式。演示代码如下:

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable(){ // 匿名实例
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
                }
            }
        },"MyThread");

        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
        }
    }
}

2. 通过 Lambda 表达式优雅创建 Runnable 线程目标类

回顾一下 Runnable 接口的源码,其中还有一个小玄机,具体如下:

java 复制代码
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

源码的小玄机为:在 Runnable 接口上声明了一个 @FunctionalInterface 注解。该注解的作用是:标记 Runnable 接口是一个函数式接口。在 Java 中。函数式接口是有且仅有一个抽象方法的接口。反过来说,如果接口有两个及以上的接口就不能使用 @FunctionalInterface 注解,否则会编译报错。

说明:@FunctionalInterface 注解不是必需的,只要符合函数式接口的定义,不加注解也是可以的。

Runnable 接口是函数式接口,在接口实现时可以使用 Lambda 表达式提供匿名实现,编写出比较优雅的代码,代码如下:

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) {
        
        Thread thread = new Thread(() -> { // lambda表达式
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
            }
        },"MyThread");

        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread name: " + Thread.currentThread().getName() + " i: " + i);
        }
    }
}

通过 Lambda 表达式直接编写的 Runnable 接口 run() 方法的实现代码,接口的名称与方法的名称全部被省略掉,只保留方法的参数列表与方法体,这样代码可以做到极致的简化。

2.5 线程创建方法三:使用 Callable 和 FutureTask 创建线程

前面已经介绍了继承 Thread 类和实现 Runnable 接口这两种方式来创建线程类,但是这两种方式都有一个共同的缺陷:不能获取异步执行的结果。

这是一个比较大的问题,很多场景都需要获取异步执行的结果,为了解决异步执行结果的问题,Java 语言在 1.5 版本之后提供了一种新的线程创建方法:通过 Callable 接口和 FutureTask 类相结合创建线程。

1. Callable 接口

Callable 接口位于 java.util.concurrent 包中,查看它的Java源代码,如下:

java 复制代码
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable 接口是一个泛型接口,也是一个函数式接口。其唯一的抽象方法 call() 有返回值,返回值的类型为 Callable 接口的泛型参数类型。 call() 抽象方法还有一个 Exception 的异常声明,允许方法的实现版本的内部异常直接抛出,并且可以不予捕捉。

问题:Callable 接口能否和 Runnable 实例一样,作为 Thread 线程实例的 target 来使用呢?

答案是不行的。Thread 的 target 属性的类型为 Runnable,而 Callable 接口与 Runnable 接口之间没有任何继承关系,并且二者唯一的方法在名字上也不相同。显而易见,Callable 接口实例是没有办法作为 Thread 线程实例的 target 来使用的。既然如此,那么该如何使用 Callable 接口去创建线程呢?一个在 Callable 接口与 Thread 实例之间起到桥梁作用的重要接口马上就登场了。

2. RunnableFuture 接口

这个重要中间搭桥接口就是 RunnableFuture 接口,该接口与 Runnable 接口、Thread类紧密相关。与 Callable 接口一样,RunnableFuture接口也位于 java.util.concurrent 包中。

RunnableFuture 是如何在 Callable 与 Thread 之间实现搭桥功能的呢?RunnableFuture 接口实现了两个目标:一是可以作为 Thread 线程实例的 target 实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture 的接口的代码:

java 复制代码
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

通过源代码可以看出,RunnableFuture 接口继承了 Runnable 接口 ,从而保证了其实例可以作为 Thread 实例的target的目标;同时,RunnableFuture 接口继承 Future 接口,保证了通过它可以获取未来的异步执行结果。

在这里,一个新的,从来没有介绍过的,非常重要的 Future 接口马上登场。

3. Future 接口

Future 接口至少提供了三大功能:

  1. 能够取消异步执行中的任务。
  2. 判断异步任务是否执行完成。
  3. 获取异步任务完成后的执行结果。
java 复制代码
public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

对 Future 接口的主要方法详细说明如下:

  1. V get() :获取异步任务的结果。注意:这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程会一直阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。
  2. V get(long timeout, TimeUnit unit) :该方法也是阻塞性的,但是结果获取线程会有一个阻塞时长限制,不会无限制的阻塞和等待,如果其阻塞时间超过设定的 timeout 时间,该方法将抛出一个异常,调用线程可以捕捉此异常。
  3. boolean isDone() :获取异步任务的执行状态。如果任务执行结束,就返回 true。
  4. boolean isCancelled() :获取异步任务的取消状态。如果任务完成前被取消,就返回 true。
  5. boolean cancel(boolean mayInterruptIfRunning) :取消异步任务的执行。

总的来说,Future 是一个对异步任务进行交互,操作的接口。但是 Future 仅仅是一个接口,通过它没有办法直接完成对异步任务的操作,JDK 提供了一个默认的实现类 ----- FutureTask

4. FutureTask 类

FutureTask 类是 Future 接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask 类不仅仅实现了 Future 接口,还实现了 Runnable 接口,准确的说,FutureTask 类实现了 RunnableFuture 接口。

前面提到的 RunnableFuture 接口很关键,是 Thread 和 Callable 之间一个很重要的桥梁角色。但 RunnableFuture 只是一个接口,无法直接创建对象,如果需要创建对象,就要用到它的实现类 ----- FutureTask。所以说,FutureTask 类才是 Thread 和 Callable 之间真正的桥梁类。

那 FutureTask 如何完成多线程的并发执行,任务结果的异步获取呢?FutureTask 内部有一个 Callable 类型的成员,具体如下:

java 复制代码
public class FutureTask<V> implements RunnableFuture<V> {
    private Callable<V> callable;
    private Object outcome; 
    
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
}

callable 实例属性用来保存并发执行的 Callable 类型的任务,并且 callable 实例属性需要在 FutureTask 实例构造时进行初始化。FutureTask 类实现了 Runnable 接口,在其 run() 方法的实现版本中会执行 callable 成员的 call() 方法。

此外,FutureTask 内部还有一个非常重要的 Object 类型的成员 ----- outcome实例属性。

FutureTask 的outcome 实例属性用来保存 callable 成员 call() 方法的异步执行结果。在 FutureTask 类 run() 方法完成 callable 成员的 call() 方法执行之后,其结果将被保存在 outcome 实例属性中,供 FutureTask 类的 get() 方法获取。

5. 使用 Callable 和 FutureTask 创建线程的具体步骤

通过 Callable 接口和 FutureTask 类的联合使用可以创建出获取异步执行结果的线程。具体步骤如下:

  1. 创建一个 Callable 接口的实现类,并实现其 call() 方法,编写好异步执行的具体逻辑,可以有返回值。
  2. 使用 Callable 实现类的实例构造一个 FutureTask 实例。
  3. 使用 FutureTask 实例作为 Thread 构造器的 target 入参,构造新的 Thread 线程实例。
  4. 调用 Thread 实例的 start() 方法启动新线程,启动新线程的 run() 方法并发执行。其内部的执行过程为:启动 Thread 实例的 run() 并发执行后,会执行 FutureTask 实例的 run() 方法,最终会并发执行 Callable 实现类的 call() 方法。
  5. 调用FutureTask 对象的 get() 方法阻塞性的获得并发线程的执行结果。

演示代码如下:

java 复制代码
import java.util.concurrent.*;

public class ThreadDemo implements Callable<Integer> {
    @Override
    public Integer call(){
        return 100;
    }

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<>(new ThreadDemo());
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            Integer result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
2.6 线程创建方法四:通过线程池创建线程

前面的示例中,所创建的 Thread 实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本,资源消耗上都是很高的(稍后会介绍),在高并发的场景中,断然不能频繁进行线程实例的创建和销毁,而是对已经创建好的线程实例进行复用,这就涉及线程池的技术。Java 中提供了一个静态工厂来创建不同的线程池,该静态工厂为 Executors 工厂。

1. 线程池的创建与执行目标提交

通过 Executors 工厂类创建一个线程池,代码示例如下:

java 复制代码
private static ExecutorService executorService = Executors.newFixedThreadPool(3);

以上实例通过工厂类 Executors 的 newFixedThreadPool() 方法创建了一个线程池,所创建的线程池类型为 ExecutorService 。

ExecutorService 是Java提供的一个线程池接口,每次我们在异步执行 target 目标任务时,可以通过 ExecutorService 线程池实例去提交或者执行。ExecutorService 实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的利用率,同时提供定时执行,定频执行,单线程,并发书控制等功能。

向 ExecutorService 线程池提交异步执行 target 目标任务常用的方法有:

  1. void execute(Runnable command) : 执行一个 Runnable 类型的 target 执行目标实例,无返回。
  2. Future submit(Callable task) : 提交一个 Callable 类型的 target 执行目标实例,返回一个 Future 异步任务实例。
  3. Future submit(Runnable task) : 提交一个 Runnable 类型的 target 执行目标实例,返回一个 Future 异步任务实例。

2. 线程池使用实践

java 复制代码
import java.util.concurrent.*;

public class ThreadDemo {

    private static final ExecutorService executorService = Executors.newFixedThreadPool(3);
    private static  int count = 0;

    private static void addCount(){
        count++;
    }

    public static void main(String[] args) {

        Callable<Integer> callable1 = () -> {
            for(int i=0;i<10000;i++){
                addCount();
            }
            return count;
        };

        Callable<Integer> callable2 = () -> {
            for(int i=0;i<10000;i++){
                addCount();
            }
            return count;
        };

        Future<Integer> future1 = executorService.submit(callable1);
        Future<Integer> future2 = executorService.submit(callable2);
        try {
            Integer result1 = future1.get();
            Integer result2 = future2.get();
            System.out.println(result1);
            System.out.println(result2);
            System.out.println(count);
        } catch (InterruptedException | ExecutionException e) {
            System.out.println(e.getMessage());
        }
        finally {
            executorService.shutdown();
        }
    }
}

说明:实际生产环境禁止使用 Executors 创建线程池。

相关推荐
散峰而望2 小时前
OJ 题目的做题模式和相关报错情况
java·c语言·数据结构·c++·vscode·算法·visual studio code
董世昌412 小时前
HTTP协议中,GET和POST有什么区别?分别适用什么场景?
java·开发语言·前端
独自破碎E2 小时前
Java中HashMap的默认负载因子为什么设置为0.75?
java·开发语言·网络
疋瓞2 小时前
C/C++查缺补漏《5》_智能指针、C和C++中的数组、指针、函数对比、C和C++中内存分配概览
java·c语言·c++
幽络源小助理2 小时前
SpringBoot+Vue大学城水电管理系统源码 | 后勤设备管理 | 幽络源
java·开发语言
黎雁·泠崖2 小时前
Java数组进阶:内存图解+二维数组全解析(底层原理+Java&C差异对比)
java·c语言·开发语言
Remember_9932 小时前
【JavaSE】一站式掌握Java面向对象编程:从类与对象到继承、多态、抽象与接口
java·开发语言·数据结构·ide·git·leetcode·eclipse
小园子的小菜2 小时前
Spring事务失效9大场景(Java面试高频)
java·spring·面试
向前V2 小时前
Flutter for OpenHarmony数独游戏App实战:胜利弹窗
java·flutter·游戏