创建线程的四种方式
创建线程的四种方式包括使用继承 Thread 类、实现 Runnable 接口、使用 Callable 和 Future 接口以及利用线程池。每种方式都有其特定的优势和适用场景。通过继承 Thread 类或实现 Runnable 接口,可以定义线程要执行的任务,并通过调用 start() 方法启动线程。使用 Callable 和 Future 接口可以在执行任务后返回结果,并且可以捕获异常。而线程池提供了一种管理和复用线程的机制,可以有效地控制并发线程的数量,并提高程序的性能和响应速度。选择合适的方式取决于应用的需求、性能要求以及对线程生命周期的管理需求。
- 继承 Thread 类:
- 这种方式是通过创建一个继承自 Thread 类的子类来实现的。子类需要重写 Thread 类的 run() 方法来定义线程要执行的任务。
- 优点是简单直观,适用于简单的线程任务。但缺点是 Java 不支持多重继承,因此如果已经继承了其他类,就无法使用这种方式。
- 实现 Runnable 接口:
- 这种方式是创建一个实现了 Runnable 接口的类,并实现其 run() 方法来定义线程要执行的任务。
- 与继承 Thread 类相比,实现 Runnable 接口的方式更灵活,因为 Java 允许类实现多个接口,而不同于单一继承的限制。
- 使用 Callable 和 Future:
- Callable 接口类似于 Runnable 接口,但它可以返回一个结果,并且可以抛出一个异常。它的 call() 方法类似于 Runnable 接口的 run() 方法。
- 这种方式允许线程在执行完任务后返回一个结果,也可以捕获异常。同时,可以通过 Future 接口来获取任务的执行结果。
- 使用线程池:
- 线程池是一种管理和复用线程的机制。通过 Executors 工厂类可以创建不同类型的线程池。
- 线程池可以控制并发线程的数量,避免因为线程频繁创建和销毁带来的性能开销。它可以有效地管理系统资源,并提高程序的性能和响应速度。
- 使用线程池可以避免手动创建和管理线程带来的麻烦,同时可以更好地控制并发度和资源消耗。
1.通过继承Thread类创建线程
通过 Thread 来创建线程,需要做两件事情
- 继承Thread类
- 重写run()方法
下面通过一个案例来演示如何通过继承Thread创建一个线程
class MyThread extends Thread {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
MyThread(){
}
// 定义一个构造方法,用于设置当前线程的名字
MyThread(String name) {
Thread.currentThread().setName(name);
}
// 获取线程名称
static String getThreadName(){
return Thread.currentThread().getName();
}
@Override
public void run() {
// 这里定义了一个循环,循环5次,每次打印当前线程名和i的值
for (int i = 0; i < 5; i++) {
logger.error("当前线程名:{},当前i的值{}", getThreadName(), i);
}
}
}
@Test
public void test() throws InterruptedException {
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
myThread1.start();
myThread2.start();
// 这里主线程最大休眠时间,保证myThread1和myThread2线程都能执行完
Thread.sleep(Integer.MAX_VALUE);
}
运行实例,结果如下:
- MyThread 类定义:
- MyThread 类包含了一个默认构造方法和一个带有 String 参数的构造方法,用于设置当前线程的名字。
- 还有一个静态方法
getThreadName()
,用于获取当前线程的名称。 - 在
run()
方法中,定义了一个循环,循环5次,每次打印当前线程名和变量 i 的值。
- 执行流程:
- 首先,当创建 MyThread 类的实例时,可以选择使用默认构造方法或者带有 String 参数的构造方法来设置线程的名字。
- 在调用 start() 方法启动线程后,线程进入就绪状态并等待 CPU 分配时间片。
- 当线程获取到 CPU 时间片后,它开始执行 run() 方法中的循环。
- 在循环中,会调用
getThreadName()
方法获取当前线程的名称,并结合循环变量 i 打印当前线程名和 i 的值。 - 循环执行 5 次后,run() 方法结束,线程执行完毕。
通过调用 start() 方法启动线程后,系统会自动调用 run() 方法,在 run() 方法中定义了线程要执行的任务。在本例中,任务是打印当前线程名和循环变量 i 的值。
2.通过实现Runnable接口来创建线程目标类
通过继承Thread类并且重写run方法只是创建Java多线程的一种方式,那么还有没有别的方式可以创建多线程?答案肯定是有的,那么我们来观察一下Thread这个类的源码。
无参构造方法、
/** * Allocates a new {@code Thread} object. This constructor has the same * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread} * {@code (null, null, gname)}, where {@code gname} is a newly generated * name. Automatically generated names are of the form * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer. */ public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }
- public Thread():这是构造方法的声明,它是公共的(public),意味着其他类可以访问和调用这个构造方法。
- init(null, null, “Thread-” + nextThreadNum(), 0):这是构造方法的实际执行部分。在这里,调用了一个名为 init 的方法,该方法用于初始化线程对象。
- init 方法参数解释:
- 参数1:这里传入了 null,表示线程要运行的目标方法为 null,即当前线程没有指定要运行的目标方法。
- 参数2:也是传入了 null,表示线程的上下文类加载器为 null。
- 参数3:“Thread-” + nextThreadNum(),这部分是为新创建的线程设置默认名称。nextThreadNum() 方法会返回下一个可用的线程编号,从而保证每个线程有唯一的名称。
- 参数4:0,表示新线程的优先级。在这里,将新线程的优先级设置为默认值 0。
- 作用:这个构造方法主要用于创建一个新的线程对象,并设置了线程的一些基本属性,如线程名称和优先级。这个构造方法在创建新线程时使用,如果开发者没有显式地指定线程的目标方法和上下文类加载器,它们就会采用默认值。
有参数构造
/** * Allocates a new {@code Thread} object. This constructor has the same * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread} * {@code (null, target, gname)}, where {@code gname} is a newly generated * name. Automatically generated names are of the form * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer. * * @param target * the object whose {@code run} method is invoked when this thread * is started. If {@code null}, this classes {@code run} method does * nothing. */ public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
这是 Java 中 Thread 类的另一个构造方法,它接受一个 Runnable 对象作为参数。让我们来解释一下这段代码:
- public Thread(Runnable target):这是构造方法的声明,它接受一个实现了 Runnable 接口的对象作为参数。
- init(null, target, “Thread-” + nextThreadNum(), 0):这是构造方法的实际执行部分。在这里,调用了一个名为 init 的方法,该方法用于初始化线程对象。
- init 方法参数解释:
- 参数1:这里传入了 null,表示线程要运行的目标方法为 null,因为此时线程的目标方法是由传入的 Runnable 对象的 run 方法确定的。
- 参数2:传入了 target,即要执行的 Runnable 对象。这个对象的 run() 方法会在新线程启动时执行。
- 参数3:“Thread-” + nextThreadNum(),这部分是为新创建的线程设置默认名称。nextThreadNum() 方法会返回下一个可用的线程编号,从而保证每个线程有唯一的名称。
- 参数4:0,表示新线程的优先级。在这里,将新线程的优先级设置为默认值 0。
- 作用:这个构造方法用于创建一个新的线程对象,并指定了线程的目标任务,即要执行的 Runnable 对象。当新线程启动时,它会调用传入 Runnable 对象的 run() 方法来执行任务。如果开发者没有指定线程组,它会使用默认的线程组。
通过这段代码,我们可以发现,我们只需要传递给Thread target目标实例(Runnable实例),就可以直接通过Thread类中的run方法以默认方式实现,达到并发执行线程的目的,这个时候,我们就可以不通过继承Thread类来实现线程类的创建。
在了解如何为target传入 Runnable实例前,我们先了解一下什么是Runnable接口
2.1. Runnable接口
Runnable接口在Java中的定义也是非常的简单
简单地说,Runnable 接口是 Java 中的一个接口,用于表示可执行的任务。它是一个函数式接口,只包含一个抽象方法
run()
,该方法定义了线程要执行的任务。通过实现 Runnable 接口并重写其 run() 方法,可以定义线程的行为,然后将该对象传递给 Thread 类或者线程池的实例的target属性后,Runnable接口的run方法将会被异步调用。使用 Runnable 接口的优势在于它可以与其他类继承和实现,不限制类的继承关系,提高了代码的灵活性和可维护性。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
2.2.通过Runnable接口创建线程类
其实就是通过实现Runnable接口,然后将需要执行的异步任务的逻辑代码放到run()方法中,将Runnable实例作为target传入Thread实例中
- 创建一个实现了 Runnable 接口的类:首先,你需要创建一个类,并让它实现 Runnable 接口。这个接口只包含一个抽象方法 run(),用于定义线程要执行的任务。
- 实现 run() 方法:在实现了 Runnable 接口的类中,你需要重写 run() 方法。这个方法里面包含了线程要执行的具体逻辑。当线程启动时,就会调用这个方法。
- 创建线程对象:在你的应用程序中,实例化一个 Thread 对象,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造方法。这个 Thread 对象代表了一个新的线程。
- 启动线程:调用线程对象的 start() 方法来启动线程。当线程启动后,它会调用 Runnable 对象的 run() 方法,并在新线程中执行定义的任务。
class MyThread2 implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* 重写run方法
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
logger.error("当前线程名:{},当前i的值{}", Thread.currentThread().getName(), i);
}
}
}
@Test
public void test() throws InterruptedException {
// 创建MyThread实例
MyThread2 myThread2 = new MyThread2();
// 创建Thread实例| 并且将MyThread实例传递给Thread,并且设置线程名
Thread thread = new Thread(myThread2,"my-thread-2-线程");
thread.start();
// 这里主线程最大休眠时间,保证myThread1和myThread2线程都能执行完
Thread.sleep(Integer.MAX_VALUE);
}
上面这段代码使用了 Runnable 接口创建线程。
- MyThread2 类实现了 Runnable 接口:
- MyThread2 类实现了 Runnable 接口,并重写了 run() 方法。
- 在 run() 方法中,定义了一个循环,循环5次,每次打印当前线程名和变量 i 的值。
- 测试方法 test():
- 在测试方法中,首先创建了 MyThread2 的实例 myThread2。
- 然后,创建了一个 Thread 实例 thread,并将 myThread2 实例作为参数传递给 Thread 构造方法。
- 在创建 Thread 实例时,还指定了线程名为 “my-thread-2-线程”。
- 最后,调用 thread 的 start() 方法启动线程。
- Thread.sleep(Integer.MAX_VALUE):
- 为了确保 myThread2 线程能够执行完毕,测试方法使用了 Thread.sleep(Integer.MAX_VALUE) 来使主线程休眠。
- Integer.MAX_VALUE 是 Java 中 int 类型的最大值,这样设置可以让主线程休眠一个非常长的时间,以保证其他线程有足够的时间执行完毕。
2.3.更优雅的创建Runnable线程目标类的两种方式
2.3.1.通过匿名类创建Runnable线程目标类
在实现Runnable的编写时target执行目标类时,如果target实现类,是一次性类,可以通过使用匿名实例的形式。
下面我来通过匿名类,来创建一个线程
@Test
public void test2() throws InterruptedException {
// 通过匿名内部了剋创建线程
// 匿名类的好处是不用创建一个新的类,但是只能创建一个实例。
// 因为没有类名,所以无法重复创建,只能创建一次,所以匿名类一般用于只需要创建一次的类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
logger.error("当前线程名:{},当前i的值{}", Thread.currentThread().getName(), i);
}
}
},"my-thread-3-线程").start();
}
- 创建线程并使用匿名内部类:
- 在 test2() 方法中,使用了匿名内部类的方式创建了一个新的线程。
- 在匿名内部类中,实现了 Runnable 接口的 run() 方法,并在其中定义了线程要执行的任务逻辑。
- 因为匿名类没有类名,所以无法重复创建,只能创建一次。因此,匿名类一般用于只需要创建一次的类,这在本例中是非常适用的。
- 启动线程:
- 在匿名内部类的结尾,调用了 start() 方法,启动了新创建的线程。
- 线程执行任务:
- 当线程启动后,会调用匿名内部类中定义的 run() 方法,并在新线程中执行其中的任务逻辑。
- 在这个例子中,任务逻辑是一个循环,循环5次,每次打印当前线程名和变量 i 的值。
2.3.2.使用Lambda表达式创建Runnable线程目标类
我们来回顾一下Runnable接口
@FunctionalInterface public interface Runnable { public abstract void run(); }
在Runnable接口上声明了一个
@FunctionalInterface
注解,该注解的作用是标记Runnable接口是一个函数式接口
,函数式接口是指有且只有一个抽象方法的接口
,如果有多个抽象方法,那么使用@FunctionalInterface
注解编译时会报错注意@FunctionalInterface 注解并不是必须的,只要一个接口有且只有一个抽象方法,那么就符合函数式接口,加不加@FunctionalInterface都是可以的,@FunctionalInterface只是作为一个编译时检查的标记
Runnable接口是一个函数式接口,那么在接口实现的时候,可以使用Lambda表达式提供匿名实现,来对代码进行简化
下面通过一个案例 来讲解一下如何使用Lambda表达式
@Test
public void test3() throws InterruptedException {
// 通过Lambda表达式创建线程
// Lambda表达式是一种匿名函数,可以理解为一段可以传递的代码
// 通过Lambda表达式创建线程,可以省略实现Runnable接口的匿名类
// 相对于匿名类,Lambda表达式更加简洁,但是Lambda表达式只能用于函数式接口,即只有一个抽象方法的接口
new Thread(() -> {
for (int i = 0; i < 5; i++) {
logger.error("当前线程名:{},当前i的值{}", Thread.currentThread().getName(), i);
}
},"my-thread-4-线程").start();
}
2.4.实现Runnable接口方式创建线程目标的优缺点
使用实现 Runnable 接口方式创建线程有以下优点和缺点
优点:
- 避免单继承限制: Java 中一个类只能继承一个父类,但是可以实现多个接口。通过实现 Runnable 接口创建线程,避免了单继承的限制,使得代码更加灵活。
- 代码解耦: 将线程任务逻辑与线程本身解耦,使得代码更易于理解、维护和扩展。因为可以把任务逻辑封装在不同的类中,使得代码结构更清晰。
- 适用于资源共享: 当多个线程需要共享同一份资源时,实现 Runnable 接口更加合适。因为可以将共享资源通过构造函数或者其他方式传递给线程对象。
- 线程池支持: 通过实现 Runnable 接口创建的线程可以被放入线程池中管理,利用线程池可以更好地控制并发线程的数量,提高系统的性能和资源利用率。
缺点:
- 稍复杂: 相比继承 Thread 类创建线程,使用实现 Runnable 接口方式稍微复杂一些,因为需要创建一个实现了 Runnable 接口的类,并实现其中的 run() 方法。
- 无法直接访问线程的方法和属性: 使用实现 Runnable 接口方式创建的线程对象不能直接访问 Thread 类的方法和属性,需要通过 Thread 类的构造方法传入。
通过Runnable接口方式创建线程目标类,更加适合多个线程的代码去处理共享资源场景
。下面我们通过一个案例来加深一下理解
3.使用Callable和Future创建线程
在前面,我们介绍了继承Thread类或者实现Runnable接口这两种方式创建线程,但是这两种方式都有一个共同的缺点,不能通过异步获取线程执行的结果。
但是,这样会出现一个很大问题,因为我们在很多场景下,需要获取异步执行的结果,通过Runnable是无法获取返回值的,因为run()方法是没有返回值的
为了解决异步执行结果的问题,JDK1.5版本后,提供了一个新的多线程的创建方式。
通过Callable接口 和 FutureTask类来相结合创建线程
下面我们来看一下Callable接口 和 FutureTask
3.1.Callable接口
Callable
接口是 Java 中的一个函数式接口,它允许你定义一个可调用的任务,类似于 Runnable
接口。然而,与 Runnable
不同的是,Callable
接口支持在任务执行完成后返回一个结果,同时可以抛出一个异常。
Callable
接口通常与 ExecutorService
结合使用,通过 ExecutorService
提供的线程池来执行 Callable
对象表示的任务。ExecutorService
是一个接口,它提供了一种管理线程的方式,可以创建、执行和管理线程,以及管理任务的执行过程。
与 Runnable
不同,Callable
的 call()
方法可以返回一个结果,这个结果的类型由泛型 V
定义。当任务执行完成时,call()
方法会返回一个结果,你可以通过 Future
对象来获取该结果。
Callable
接口位于 java.util.concurrent
包中,它声明了一个单一的方法 call()
,该方法在调用时可以返回一个结果,或者在执行过程中抛出一个异常。
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
问题?Callable接口能否像Runnable实例一样,作为Thread线程的实例的target来使用呢?
答案是不行的
Callable
接口与Runnable
接口有所不同,因为Callable
接口的call()
方法可以返回一个结果,而Runnable
接口的run()
方法不返回任何结果。因此,直接将
Callable
对象作为Thread
的构造函数的参数是不可行的,因为Thread
的构造函数需要一个Runnable
对象,而不是Callable
对象。这个时候就需要在Callable接口 和 Thread线程之前搭建桥梁,那么下面我来介绍一下一个新的接口
RunnableFuture
3.2.RunnableFuture接口
RunnableFuture
是一个接口,它继承自 Runnable
和 Future
接口。在 Java 中,Runnable
用于表示一个可以由线程执行的任务,而 Future
用于表示一个异步计算的结果。RunnableFuture
结合了这两个概念,表示一个可以由线程执行并且可以获取结果的任务。
RunnableFuture
接口定义了一个方法 run()
,它继承自 Runnable
接口,用于执行任务。此外,它还继承了 Future
接口,提供了一些方法来获取任务执行的结果。
这个接口的常见实现类是 FutureTask
。FutureTask
实现了 RunnableFuture
接口,因此它可以作为一个可执行的任务提交给线程池,同时又可以通过 Future
的方法获取任务执行的结果。
以下是 RunnableFuture
接口的声明:
package java.util.concurrent;
/**
* A {@link Future} that is {@link Runnable}. Successful execution of
* the {@code run} method causes completion of the {@code Future}
* and allows access to its results.
* @see FutureTask
* @see Executor
* @since 1.6
* @author Doug Lea
* @param <V> The result type returned by this Future's {@code get} method
*/
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
RunnableFuture
并没有额外的方法,因为它只是将 Runnable
和 Future
这两个接口结合起来,以便能够表示一个可以执行的任务,并且可以获取任务执行结果的对象。
在多线程编程中,使用 RunnableFuture
可以方便地表示可执行的任务,同时又可以获取任务执行的结果,这在很多并发编程的场景中非常有用。
3.3.Future接口
下面我来介绍一下主角Future
Future接口,至少提供了三大功能
- 能够取消异步执行中的任务
- 判断异步任务是否完成
- 获取异步执行的结果
Future接口的定义
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;
}
主要方法
boolean cancel(boolean mayInterruptIfRunning)
: 用于取消任务的执行。如果任务已经完成、已经被取消或者由于其他原因不能取消,那么这个方法会返回 false。如果任务尚未启动,它将被中断,除非mayInterruptIfRunning
参数被设置为 false。boolean isCancelled()
: 如果任务在完成前被取消,则返回 true。boolean isDone()
: 如果任务已经完成,则返回 true。V get() throws InterruptedException, ExecutionException
: 获取计算的结果。如果计算尚未完成,调用此方法会阻塞当前线程直到计算完成。如果计算被取消,会抛出CancellationException
。如果计算抛出异常,会抛出ExecutionException
。V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
: 在指定的时间内获取计算的结果。如果计算尚未完成,调用此方法会阻塞当前线程直到计算完成或超时。如果计算被取消,会抛出CancellationException
。如果计算抛出异常,会抛出ExecutionException
。
能够与线程池的结合使用
Future
接口通常与线程池一起使用。当你提交一个任务到线程池时,线程池会返回一个 Future
对象,你可以使用这个对象来获取任务的执行结果。
能够监听任务的执行状态
Future
接口提供了一些方法来查询任务的执行状态,比如是否已经完成、是否已经取消等。
异常处理
通过 get()
方法获取任务执行结果时,如果任务执行过程中抛出了异常,get()
方法会抛出 ExecutionException
,你可以通过该异常来处理任务执行过程中的异常情况。
总的来说,Future是一个异步任务交互,操作的接口。但是Future仅仅只是一个接口,通过它没有办法直接完成异步的操作。JDK提供了一个默认的实现类FutureTask
3.4.FutureTask
FutureTask是 Future接口的实现类,提供了对异步任务操作的具体实现,而且FutureTask还实现了RunnableFuture接口
FutureTask
是 Java 中实现了 RunnableFuture
接口的一个类,它实现了 Runnable
和 Future
接口,允许你将一个可调用的任务提交给线程池,并在需要时获取任务的执行结果。
构造方法
public FutureTask(Callable<V> callable)
使用指定的 Callable
创建一个 FutureTask
,这个 Callable
的 call 方法将会被异步执行。
public FutureTask(Runnable runnable, V result)
使用指定的 Runnable
创建一个 FutureTask
,并指定最终的返回结果。
主要方法
void run()
- 如果此任务尚未完成,则调用其
run()
方法,运行任务,并设置其状态为已完成。注意,run()
方法只能执行一次,后续调用将不会执行任务。
- 如果此任务尚未完成,则调用其
boolean cancel(boolean mayInterruptIfRunning)
- 尝试取消此任务的执行。如果任务已经完成、已经被取消或者由于其他原因不能取消,那么这个方法会返回 false。
- 如果
mayInterruptIfRunning
参数为 true,并且任务正在执行,则会尝试中断执行此任务的线程。
boolean isCancelled()
- 如果任务在完成前被取消,则返回 true。
boolean isDone()
- 如果任务已经完成,则返回 true。
V get() throws InterruptedException, ExecutionException
- 获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成。如果任务被取消,会抛出
CancellationException
。如果任务执行过程中抛出异常,会抛出ExecutionException
。
- 获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成。如果任务被取消,会抛出
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
- 在指定的时间内获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成或超时。如果任务被取消,会抛出
CancellationException
。如果任务执行过程中抛出异常,会抛出ExecutionException
。
- 在指定的时间内获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成或超时。如果任务被取消,会抛出
3.5.使用Callable和FutureTask创建线程案例
这段代码是一个使用
Callable
和FutureTask
的示例,它计算了从 0 到 99 的所有整数的和,并在控制台打印了当前线程名和i
的值。****
- 首先定义了一个
static class MyCallable implements Callable<Integer>
,实现了Callable
接口,用于计算 0 到 99 的所有整数的和。在call()
方法中,使用一个循环来计算和,并在每次循环中打印当前线程名和i
的值。- 在
test4()
方法中,首先创建了MyCallable
的一个实例对象task
。- 然后,创建了一个
FutureTask<Integer>
的实例对象futureTask
,将task
传递给FutureTask
的构造函数。- 接着,创建了一个新的线程
thread
,将futureTask
对象作为其构造函数的参数,并给线程命名为 “my-thread-5-线程”。- 启动了
thread
线程,开始执行futureTask
中的call()
方法。- 在主线程中,调用了
Thread.sleep(3000)
,使主线程休眠 3 秒钟。这段时间内,子线程thread
在后台计算并打印结果。- 主线程休眠结束后,调用
futureTask.get()
方法获取子线程的计算结果,此方法会阻塞直到子线程执行完毕并返回结果。- 在主线程中,打印出获取到的计算结果。
- 最后,主线程休眠了
Integer.MAX_VALUE
毫秒,以保持程序的运行,防止线程退出。
static class MyCallable implements Callable<Integer> {
static int j = 0;
// 创建一个Callable接口的实现类 计算1-100的和
@Override
public Integer call() throws Exception {
int j = 0;
for (int i = 0; i < 100; i++) {
// 计算
j = j + i;
// 打印当前线程名和i的值
logger.error("当前线程名:{},当前i的值{}", Thread.currentThread().getName(), i);
}
logger.error("当前线程名:{},执行结束", Thread.currentThread().getName());
return j;
}
}
@Test
public void test4() throws InterruptedException, ExecutionException {
// 创建Callable接口的实现类
MyCallable task = new MyCallable();
// 创建FutureTask实例,将Callable接口的实现类传递给FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 创建Thread实例,并且将FutureTask实例传递给Thread
Thread thread = new Thread(futureTask,"my-thread-5-线程");
// 启动线程
thread.start();
// 休眠3s
logger.error("当前线程名:{},休眠3s", Thread.currentThread().getName());
Thread.sleep(3000);
// 休眠完成获取结果
logger.error("当前线程名:{},休眠完成,获取结果{}", Thread.currentThread().getName(),futureTask.get());
// 主线程执行结束
logger.error("当前线程名:{},执行结束", Thread.currentThread().getName());
Thread.sleep(Integer.MAX_VALUE);
}
4.通过线程池来创建线程
前面的案例中,所有创建的Treahd的实例,在执行完毕都是被销毁了,线程没法得到复用,因为创建线程和销毁线程是会占用较多的操作系统资源的。
为了解决这个问题,可以使用线程池来管理线程,实现线程的复用。Java 提供了
ExecutorService
接口和一些实现类来支持线程池的创建和管理。
这里针对线程池 只是作为一个了解,在后面的文章中,会详细介绍线程的使用,以及注意事项
4.1.线程池的创建和目标提交
在使用线程池之前,我们先了解一下 Executors静态工厂类 和 ExecutorService
4.1.1.Executors
Executors
类是 Java 标准库提供的一个工厂类,用于创建各种类型的线程池和线程执行器。它提供了一系列静态工厂方法,使得线程池的创建变得简单和方便。
下面是 Executors
类中一些常用的静态工厂方法:
newFixedThreadPool(int nThreads)
:- 创建一个固定大小的线程池,线程池中的线程数量固定为指定的
nThreads
数量。 - 当有新的任务提交时,如果线程池中的线程数小于
nThreads
,则会创建新的线程来处理任务;如果线程池中的线程数已经达到nThreads
,则新任务会被放入任务队列中等待。 - 适用于负载比较固定的情况。
- 创建一个固定大小的线程池,线程池中的线程数量固定为指定的
newCachedThreadPool()
:- 创建一个可缓存的线程池,线程池中的线程数量不固定,可以根据需要创建新的线程。
- 如果线程池中的线程在执行任务时空闲超过 60 秒,则会被终止并从线程池中移除。
- 适用于执行大量的短期异步任务的情况。
newSingleThreadExecutor()
:- 创建一个单线程的线程池,该线程池保证所有任务按顺序执行,即每次只有一个线程在执行任务。
- 适用于需要保证任务按顺序执行的情况,比如任务之间有依赖关系或需要线程安全的操作。
newScheduledThreadPool(int corePoolSize)
:- 创建一个可以执行定时任务的线程池,线程池中的线程数量固定为指定的
corePoolSize
数量。 - 适用于需要定时执行任务的情况,比如定时任务调度、定时数据处理等。
- 创建一个可以执行定时任务的线程池,线程池中的线程数量固定为指定的
Executors
类中的这些静态工厂方法都返回了实现了 ExecutorService
接口的线程池实例,使得开发者可以方便地创建并使用不同类型的线程池。需要注意的是,在某些情况下,直接使用 ThreadPoolExecutor
类可能更加灵活,因为它允许对线程池的底层参数进行更精细的控制。
4.1.2.ExecutorService
ExecutorService
接口是 Java 并发编程中的一个重要接口,它扩展了 Executor
接口,提供了更丰富的功能,用于管理和控制异步任务的执行。
以下是 ExecutorService
接口的一些关键特性和方法:
- 任务提交和执行:
submit(Runnable task)
:提交一个Runnable
任务给线程池执行,并返回一个Future
对象,通过该对象可以获取任务的执行结果或取消任务的执行。submit(Callable<T> task)
:提交一个Callable
任务给线程池执行,并返回一个Future
对象,通过该对象可以获取任务的执行结果或取消任务的执行。
- 任务执行控制:
shutdown()
:平缓地关闭线程池,不再接受新的任务,但会等待已经提交的任务执行完成。shutdownNow()
:立即关闭线程池,并尝试中断正在执行的任务。awaitTermination(long timeout, TimeUnit unit)
:等待线程池中的所有任务执行完毕并关闭,或者在指定的超时时间内等待。
- 任务执行结果获取:
Future<T> submit(Callable<T> task)
:提交一个Callable
任务给线程池执行,并返回一个Future
对象,通过该对象可以获取任务的执行结果或取消任务的执行。Future<?> submit(Runnable task)
:提交一个Runnable
任务给线程池执行,并返回一个Future
对象,通过该对象可以获取任务的执行结果或取消任务的执行。<T> invokeAny(Collection<? extends Callable<T>> tasks)
:执行给定的任务集合中的一个任务,返回首次成功执行完成的任务的结果,并取消所有其他任务。<T> invokeAll(Collection<? extends Callable<T>> tasks)
:执行给定的任务集合中的所有任务,并返回一个包含所有任务执行结果的Future
列表。
- 线程池状态和信息查询:
isShutdown()
:判断线程池是否已经关闭。isTerminated()
:判断线程池是否已经终止。isTerminating()
:判断线程池是否正在关闭过程中。
通过 ExecutorService
接口,我们可以更加灵活地管理和控制线程池的执行,可以提交各种类型的任务,并通过 Future
对象来获取任务的执行结果或取消任务的执行。 ExecutorService
接口的实现类提供了一系列方法来操作线程池,并提供了对任务执行的控制和监控功能,是 Java 并发编程中的一个核心组件。
下面通过一个案例来了解如何通过线程池创建线程,还是刚刚计算 从1-100的累加之和,我们只是换种方式进行是实现
@Test
public void test5(){
// 创建一个可重用固定线程数的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 创建一个 Callable 对象
Callable<Integer> callableTask = new Callable<Integer>() {
public Integer call() {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
logger.error("当前线程名:" + Thread.currentThread().getName() + ",当前i的值:" + i);
}
logger.error("当前线程名:" + Thread.currentThread().getName() + ",执行结束");
return sum;
}
};
// 提交任务到线程池并获取 Future 对象
Future<Integer> future = executor.submit(callableTask);
// 在主线程中可以进行其他操作,不必等待任务完成
// 获取任务执行结果
int result = 0;
try {
result = future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
logger.error("主线程获取到的计算结果:" + result);
// 关闭线程池
executor.shutdown();
}
- 首先,通过
Executors.newFixedThreadPool(5)
创建了一个固定大小为 5 的线程池executor
。 - 然后,创建了一个
Callable
对象callableTask
,用于计算 0 到 99 的整数的和。在call()
方法中,打印了当前线程名和i
的值。 - 接着,通过
executor.submit(callableTask)
方法将任务提交给线程池执行,并得到了一个Future
对象future
。 - 在主线程中,可以进行其他操作,不必等待任务完成。
- 主线程调用
future.get()
方法获取任务执行结果。这个方法会阻塞主线程,直到任务执行完成并返回结果。在此期间,线程池中的线程执行任务,并打印了相应的日志信息。 - 任务执行完成后,主线程继续执行,获取到任务的执行结果
result
。 - 最后,调用
executor.shutdown()
方法关闭线程池,释放资源。
总结:这段代码的执行流程是先创建线程池,然后将任务提交给线程池执行,主线程获取任务执行结果,最后关闭线程池。通过使用 ExecutorService
和 Future
,我们可以更加灵活地管理和控制异步任务的执行,并且能够获取任务的执行结果。
5.总结
上面学习了四种创建线程的方式,下面我最后来总结比较一下四种创建线程的方式
在 Java 中,创建线程的方式有四种:继承 Thread
类、实现 Runnable
接口、实现 Callable
接口配合 Future
和 ExecutorService
、使用 Executor
框架。每种方式都有其优点和缺点,下面进行简要比较:
- 继承
Thread
类:- 优点:
- 简单直观,易于理解和使用。
- 缺点:
- 因为 Java 是单继承的,如果继承
Thread
类,就无法继承其他类,限制了灵活性。 - 每个
Thread
实例都代表一个独立的线程,对象级别的开销较大。
- 因为 Java 是单继承的,如果继承
- 优点:
- 实现
Runnable
接口:- 优点:
- 支持多线程共享同一个实例,避免了继承单一类的限制,提高了灵活性。
- 可以避免由于单继承而带来的局限性。
- 缺点:
- 编写的代码对于线程的状态和操作不够清晰。
- 优点:
- 实现
Callable
接口配合Future
和ExecutorService
:- 优点:
- 支持返回结果和抛出异常。
- 支持取消任务执行。
- 可以获取任务执行的状态。
- 可以控制线程的数量。
- 缺点:
- 相对于实现
Runnable
接口,编写的代码更加繁琐一些。
- 相对于实现
- 优点:
- 使用
Executor
框架:- 优点:
- 提供了高度灵活的线程管理、任务调度和线程池功能。
- 可以降低线程创建和销毁的开销,提高性能。
- 可以通过统一的接口来控制和管理多线程任务。
- 缺点:
- 可能因为线程池参数配置不合理而导致资源浪费或性能下降。
- 优点: