Java并发编程-实现多线程的四种方式

创建线程的四种方式

创建线程的四种方式包括使用继承 Thread 类、实现 Runnable 接口、使用 Callable 和 Future 接口以及利用线程池。每种方式都有其特定的优势和适用场景。通过继承 Thread 类或实现 Runnable 接口,可以定义线程要执行的任务,并通过调用 start() 方法启动线程。使用 Callable 和 Future 接口可以在执行任务后返回结果,并且可以捕获异常。而线程池提供了一种管理和复用线程的机制,可以有效地控制并发线程的数量,并提高程序的性能和响应速度。选择合适的方式取决于应用的需求、性能要求以及对线程生命周期的管理需求。

  1. 继承 Thread 类
    • 这种方式是通过创建一个继承自 Thread 类的子类来实现的。子类需要重写 Thread 类的 run() 方法来定义线程要执行的任务。
    • 优点是简单直观,适用于简单的线程任务。但缺点是 Java 不支持多重继承,因此如果已经继承了其他类,就无法使用这种方式。
  2. 实现 Runnable 接口
    • 这种方式是创建一个实现了 Runnable 接口的类,并实现其 run() 方法来定义线程要执行的任务。
    • 与继承 Thread 类相比,实现 Runnable 接口的方式更灵活,因为 Java 允许类实现多个接口,而不同于单一继承的限制。
  3. 使用 Callable 和 Future
    • Callable 接口类似于 Runnable 接口,但它可以返回一个结果,并且可以抛出一个异常。它的 call() 方法类似于 Runnable 接口的 run() 方法。
    • 这种方式允许线程在执行完任务后返回一个结果,也可以捕获异常。同时,可以通过 Future 接口来获取任务的执行结果。
  4. 使用线程池
    • 线程池是一种管理和复用线程的机制。通过 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);
}

运行实例,结果如下:

image-20240306212643217

  1. MyThread 类定义
    • MyThread 类包含了一个默认构造方法和一个带有 String 参数的构造方法,用于设置当前线程的名字。
    • 还有一个静态方法 getThreadName(),用于获取当前线程的名称。
    • run() 方法中,定义了一个循环,循环5次,每次打印当前线程名和变量 i 的值。
  2. 执行流程
    • 首先,当创建 MyThread 类的实例时,可以选择使用默认构造方法或者带有 String 参数的构造方法来设置线程的名字。
    • 在调用 start() 方法启动线程后,线程进入就绪状态并等待 CPU 分配时间片。
    • 当线程获取到 CPU 时间片后,它开始执行 run() 方法中的循环。
    • 在循环中,会调用 getThreadName() 方法获取当前线程的名称,并结合循环变量 i 打印当前线程名和 i 的值。
    • 循环执行 5 次后,run() 方法结束,线程执行完毕。

通过调用 start() 方法启动线程后,系统会自动调用 run() 方法,在 run() 方法中定义了线程要执行的任务。在本例中,任务是打印当前线程名和循环变量 i 的值。

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

通过继承Thread类并且重写run方法只是创建Java多线程的一种方式,那么还有没有别的方式可以创建多线程?答案肯定是有的,那么我们来观察一下Thread这个类的源码。

  1. 无参构造方法、

    1.     /**
           * 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);
          }
      
      1. public Thread():这是构造方法的声明,它是公共的(public),意味着其他类可以访问和调用这个构造方法。
      2. init(null, null, “Thread-” + nextThreadNum(), 0):这是构造方法的实际执行部分。在这里,调用了一个名为 init 的方法,该方法用于初始化线程对象。
      3. init 方法参数解释
        • 参数1:这里传入了 null,表示线程要运行的目标方法为 null,即当前线程没有指定要运行的目标方法。
        • 参数2:也是传入了 null,表示线程的上下文类加载器为 null。
        • 参数3:“Thread-” + nextThreadNum(),这部分是为新创建的线程设置默认名称。nextThreadNum() 方法会返回下一个可用的线程编号,从而保证每个线程有唯一的名称。
        • 参数4:0,表示新线程的优先级。在这里,将新线程的优先级设置为默认值 0。
      4. 作用:这个构造方法主要用于创建一个新的线程对象,并设置了线程的一些基本属性,如线程名称和优先级。这个构造方法在创建新线程时使用,如果开发者没有显式地指定线程的目标方法和上下文类加载器,它们就会采用默认值。
  2. 有参数构造

    1.     /**
           * 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 对象作为参数。让我们来解释一下这段代码:

      1. public Thread(Runnable target):这是构造方法的声明,它接受一个实现了 Runnable 接口的对象作为参数。
      2. init(null, target, “Thread-” + nextThreadNum(), 0):这是构造方法的实际执行部分。在这里,调用了一个名为 init 的方法,该方法用于初始化线程对象。
      3. init 方法参数解释
        • 参数1:这里传入了 null,表示线程要运行的目标方法为 null,因为此时线程的目标方法是由传入的 Runnable 对象的 run 方法确定的。
        • 参数2:传入了 target,即要执行的 Runnable 对象。这个对象的 run() 方法会在新线程启动时执行。
        • 参数3:“Thread-” + nextThreadNum(),这部分是为新创建的线程设置默认名称。nextThreadNum() 方法会返回下一个可用的线程编号,从而保证每个线程有唯一的名称。
        • 参数4:0,表示新线程的优先级。在这里,将新线程的优先级设置为默认值 0。
      4. 作用:这个构造方法用于创建一个新的线程对象,并指定了线程的目标任务,即要执行的 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实例中

  1. 创建一个实现了 Runnable 接口的类:首先,你需要创建一个类,并让它实现 Runnable 接口。这个接口只包含一个抽象方法 run(),用于定义线程要执行的任务。
  2. 实现 run() 方法:在实现了 Runnable 接口的类中,你需要重写 run() 方法。这个方法里面包含了线程要执行的具体逻辑。当线程启动时,就会调用这个方法。
  3. 创建线程对象:在你的应用程序中,实例化一个 Thread 对象,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造方法。这个 Thread 对象代表了一个新的线程。
  4. 启动线程:调用线程对象的 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);
}

image-20240307073606629

上面这段代码使用了 Runnable 接口创建线程。

  1. MyThread2 类实现了 Runnable 接口
    • MyThread2 类实现了 Runnable 接口,并重写了 run() 方法。
    • 在 run() 方法中,定义了一个循环,循环5次,每次打印当前线程名和变量 i 的值。
  2. 测试方法 test()
    • 在测试方法中,首先创建了 MyThread2 的实例 myThread2。
    • 然后,创建了一个 Thread 实例 thread,并将 myThread2 实例作为参数传递给 Thread 构造方法。
    • 在创建 Thread 实例时,还指定了线程名为 “my-thread-2-线程”。
    • 最后,调用 thread 的 start() 方法启动线程。
  3. 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();
}

image-20240307074247631

  1. 创建线程并使用匿名内部类
    • 在 test2() 方法中,使用了匿名内部类的方式创建了一个新的线程。
    • 在匿名内部类中,实现了 Runnable 接口的 run() 方法,并在其中定义了线程要执行的任务逻辑。
    • 因为匿名类没有类名,所以无法重复创建,只能创建一次。因此,匿名类一般用于只需要创建一次的类,这在本例中是非常适用的。
  2. 启动线程
    • 在匿名内部类的结尾,调用了 start() 方法,启动了新创建的线程。
  3. 线程执行任务
    • 当线程启动后,会调用匿名内部类中定义的 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();
}

image-20240307075444418

2.4.实现Runnable接口方式创建线程目标的优缺点

使用实现 Runnable 接口方式创建线程有以下优点和缺点

优点:

  1. 避免单继承限制: Java 中一个类只能继承一个父类,但是可以实现多个接口。通过实现 Runnable 接口创建线程,避免了单继承的限制,使得代码更加灵活。
  2. 代码解耦: 将线程任务逻辑与线程本身解耦,使得代码更易于理解、维护和扩展。因为可以把任务逻辑封装在不同的类中,使得代码结构更清晰。
  3. 适用于资源共享: 当多个线程需要共享同一份资源时,实现 Runnable 接口更加合适。因为可以将共享资源通过构造函数或者其他方式传递给线程对象。
  4. 线程池支持: 通过实现 Runnable 接口创建的线程可以被放入线程池中管理,利用线程池可以更好地控制并发线程的数量,提高系统的性能和资源利用率。

缺点:

  1. 稍复杂: 相比继承 Thread 类创建线程,使用实现 Runnable 接口方式稍微复杂一些,因为需要创建一个实现了 Runnable 接口的类,并实现其中的 run() 方法。
  2. 无法直接访问线程的方法和属性: 使用实现 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 不同,Callablecall() 方法可以返回一个结果,这个结果的类型由泛型 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 是一个接口,它继承自 RunnableFuture 接口。在 Java 中,Runnable 用于表示一个可以由线程执行的任务,而 Future 用于表示一个异步计算的结果。RunnableFuture 结合了这两个概念,表示一个可以由线程执行并且可以获取结果的任务。

RunnableFuture 接口定义了一个方法 run(),它继承自 Runnable 接口,用于执行任务。此外,它还继承了 Future 接口,提供了一些方法来获取任务执行的结果。

这个接口的常见实现类是 FutureTaskFutureTask 实现了 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 并没有额外的方法,因为它只是将 RunnableFuture 这两个接口结合起来,以便能够表示一个可以执行的任务,并且可以获取任务执行结果的对象。

在多线程编程中,使用 RunnableFuture 可以方便地表示可执行的任务,同时又可以获取任务执行的结果,这在很多并发编程的场景中非常有用。

3.3.Future接口

下面我来介绍一下主角Future

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

  1. 能够取消异步执行中的任务
  2. 判断异步任务是否完成
  3. 获取异步执行的结果

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 接口的一个类,它实现了 RunnableFuture 接口,允许你将一个可调用的任务提交给线程池,并在需要时获取任务的执行结果。

构造方法

public FutureTask(Callable<V> callable)

使用指定的 Callable 创建一个 FutureTask,这个 Callable 的 call 方法将会被异步执行。

public FutureTask(Runnable runnable, V result)

使用指定的 Runnable 创建一个 FutureTask,并指定最终的返回结果。

主要方法

  1. void run()
    • 如果此任务尚未完成,则调用其 run() 方法,运行任务,并设置其状态为已完成。注意,run() 方法只能执行一次,后续调用将不会执行任务。
  2. boolean cancel(boolean mayInterruptIfRunning)
    • 尝试取消此任务的执行。如果任务已经完成、已经被取消或者由于其他原因不能取消,那么这个方法会返回 false。
    • 如果 mayInterruptIfRunning 参数为 true,并且任务正在执行,则会尝试中断执行此任务的线程。
  3. boolean isCancelled()
    • 如果任务在完成前被取消,则返回 true。
  4. boolean isDone()
    • 如果任务已经完成,则返回 true。
  5. V get() throws InterruptedException, ExecutionException
    • 获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成。如果任务被取消,会抛出 CancellationException。如果任务执行过程中抛出异常,会抛出 ExecutionException
  6. V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
    • 在指定的时间内获取任务的执行结果。如果任务尚未完成,调用此方法会阻塞当前线程直到任务完成或超时。如果任务被取消,会抛出 CancellationException。如果任务执行过程中抛出异常,会抛出 ExecutionException

3.5.使用Callable和FutureTask创建线程案例

这段代码是一个使用 CallableFutureTask 的示例,它计算了从 0 到 99 的所有整数的和,并在控制台打印了当前线程名和 i 的值。****

  1. 首先定义了一个 static class MyCallable implements Callable<Integer>,实现了 Callable 接口,用于计算 0 到 99 的所有整数的和。在 call() 方法中,使用一个循环来计算和,并在每次循环中打印当前线程名和 i 的值。
  2. test4() 方法中,首先创建了 MyCallable 的一个实例对象 task
  3. 然后,创建了一个 FutureTask<Integer> 的实例对象 futureTask,将 task 传递给 FutureTask 的构造函数。
  4. 接着,创建了一个新的线程 thread,将 futureTask 对象作为其构造函数的参数,并给线程命名为 “my-thread-5-线程”。
  5. 启动了 thread 线程,开始执行 futureTask 中的 call() 方法。
  6. 在主线程中,调用了 Thread.sleep(3000),使主线程休眠 3 秒钟。这段时间内,子线程 thread 在后台计算并打印结果。
  7. 主线程休眠结束后,调用 futureTask.get() 方法获取子线程的计算结果,此方法会阻塞直到子线程执行完毕并返回结果。
  8. 在主线程中,打印出获取到的计算结果。
  9. 最后,主线程休眠了 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);


}

image-20240307214641993

4.通过线程池来创建线程

前面的案例中,所有创建的Treahd的实例,在执行完毕都是被销毁了,线程没法得到复用,因为创建线程和销毁线程是会占用较多的操作系统资源的。

为了解决这个问题,可以使用线程池来管理线程,实现线程的复用。Java 提供了 ExecutorService 接口和一些实现类来支持线程池的创建和管理。

这里针对线程池 只是作为一个了解,在后面的文章中,会详细介绍线程的使用,以及注意事项

4.1.线程池的创建和目标提交

在使用线程池之前,我们先了解一下 Executors静态工厂类 和 ExecutorService

4.1.1.Executors

Executors 类是 Java 标准库提供的一个工厂类,用于创建各种类型的线程池和线程执行器。它提供了一系列静态工厂方法,使得线程池的创建变得简单和方便。

下面是 Executors 类中一些常用的静态工厂方法:

  1. newFixedThreadPool(int nThreads)
    • 创建一个固定大小的线程池,线程池中的线程数量固定为指定的 nThreads 数量。
    • 当有新的任务提交时,如果线程池中的线程数小于 nThreads,则会创建新的线程来处理任务;如果线程池中的线程数已经达到 nThreads,则新任务会被放入任务队列中等待。
    • 适用于负载比较固定的情况。
  2. newCachedThreadPool()
    • 创建一个可缓存的线程池,线程池中的线程数量不固定,可以根据需要创建新的线程。
    • 如果线程池中的线程在执行任务时空闲超过 60 秒,则会被终止并从线程池中移除。
    • 适用于执行大量的短期异步任务的情况。
  3. newSingleThreadExecutor()
    • 创建一个单线程的线程池,该线程池保证所有任务按顺序执行,即每次只有一个线程在执行任务。
    • 适用于需要保证任务按顺序执行的情况,比如任务之间有依赖关系或需要线程安全的操作。
  4. newScheduledThreadPool(int corePoolSize)
    • 创建一个可以执行定时任务的线程池,线程池中的线程数量固定为指定的 corePoolSize 数量。
    • 适用于需要定时执行任务的情况,比如定时任务调度、定时数据处理等。

Executors 类中的这些静态工厂方法都返回了实现了 ExecutorService 接口的线程池实例,使得开发者可以方便地创建并使用不同类型的线程池。需要注意的是,在某些情况下,直接使用 ThreadPoolExecutor 类可能更加灵活,因为它允许对线程池的底层参数进行更精细的控制。

4.1.2.ExecutorService

ExecutorService 接口是 Java 并发编程中的一个重要接口,它扩展了 Executor 接口,提供了更丰富的功能,用于管理和控制异步任务的执行。

以下是 ExecutorService 接口的一些关键特性和方法:

  1. 任务提交和执行
    • submit(Runnable task):提交一个 Runnable 任务给线程池执行,并返回一个 Future 对象,通过该对象可以获取任务的执行结果或取消任务的执行。
    • submit(Callable<T> task):提交一个 Callable 任务给线程池执行,并返回一个 Future 对象,通过该对象可以获取任务的执行结果或取消任务的执行。
  2. 任务执行控制
    • shutdown():平缓地关闭线程池,不再接受新的任务,但会等待已经提交的任务执行完成。
    • shutdownNow():立即关闭线程池,并尝试中断正在执行的任务。
    • awaitTermination(long timeout, TimeUnit unit):等待线程池中的所有任务执行完毕并关闭,或者在指定的超时时间内等待。
  3. 任务执行结果获取
    • 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 列表。
  4. 线程池状态和信息查询
    • 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();
	}

image-20240307220001583

  1. 首先,通过 Executors.newFixedThreadPool(5) 创建了一个固定大小为 5 的线程池 executor
  2. 然后,创建了一个 Callable 对象 callableTask,用于计算 0 到 99 的整数的和。在 call() 方法中,打印了当前线程名和 i 的值。
  3. 接着,通过 executor.submit(callableTask) 方法将任务提交给线程池执行,并得到了一个 Future 对象 future
  4. 在主线程中,可以进行其他操作,不必等待任务完成。
  5. 主线程调用 future.get() 方法获取任务执行结果。这个方法会阻塞主线程,直到任务执行完成并返回结果。在此期间,线程池中的线程执行任务,并打印了相应的日志信息。
  6. 任务执行完成后,主线程继续执行,获取到任务的执行结果 result
  7. 最后,调用 executor.shutdown() 方法关闭线程池,释放资源。

总结:这段代码的执行流程是先创建线程池,然后将任务提交给线程池执行,主线程获取任务执行结果,最后关闭线程池。通过使用 ExecutorServiceFuture,我们可以更加灵活地管理和控制异步任务的执行,并且能够获取任务的执行结果。

5.总结

上面学习了四种创建线程的方式,下面我最后来总结比较一下四种创建线程的方式

在 Java 中,创建线程的方式有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 FutureExecutorService、使用 Executor 框架。每种方式都有其优点和缺点,下面进行简要比较:

  1. 继承 Thread 类:
    • 优点:
      • 简单直观,易于理解和使用。
    • 缺点:
      • 因为 Java 是单继承的,如果继承 Thread 类,就无法继承其他类,限制了灵活性。
      • 每个 Thread 实例都代表一个独立的线程,对象级别的开销较大。
  2. 实现 Runnable 接口:
    • 优点:
      • 支持多线程共享同一个实例,避免了继承单一类的限制,提高了灵活性。
      • 可以避免由于单继承而带来的局限性。
    • 缺点:
      • 编写的代码对于线程的状态和操作不够清晰。
  3. 实现 Callable 接口配合 FutureExecutorService
    • 优点:
      • 支持返回结果和抛出异常。
      • 支持取消任务执行。
      • 可以获取任务执行的状态。
      • 可以控制线程的数量。
    • 缺点:
      • 相对于实现 Runnable 接口,编写的代码更加繁琐一些。
  4. 使用 Executor 框架:
    • 优点:
      • 提供了高度灵活的线程管理、任务调度和线程池功能。
      • 可以降低线程创建和销毁的开销,提高性能。
      • 可以通过统一的接口来控制和管理多线程任务。
    • 缺点:
      • 可能因为线程池参数配置不合理而导致资源浪费或性能下降。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/437520.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计划任务和日志

一、计划任务 计划任务概念解析 在Linux操作系统中&#xff0c;除了用户即时执行的命令操作以外&#xff0c;还可以配置在指定的时间、指定的日期执行预先计划好的系统管理任务&#xff08;如定期备份、定期采集监测数据&#xff09;。RHEL6系统中默认已安装了at、crontab软件…

javascript中的强制类型转换和自动类型转换

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;前端泛海 景天的主页&#xff1a;景天科技苑 文章目录 1.转换函数2.强制类型转换&#xff08;1&#xff09;Number类型强转&…

【蓝桥杯】路径之谜(DFS)

一.题目描述 小明冒充 X 星球的骑士&#xff0c;进入了一个奇怪的城堡。 城堡里边什么都没有&#xff0c;只有方形石头铺成的地面。 假设城堡地面是 nn 个方格。如下图所示。 按习俗&#xff0c;骑士要从西北角走到东南角。可以横向或纵向移动&#xff0c;但不能斜着走&#x…

Linux文件描述符剖析

文章目录 文件描述符文件描述符分配规则重定向软硬链接软链接&#xff08;Symbolic Link&#xff09;&#xff1a;硬链接&#xff08;Hard Link&#xff09;&#xff1a; 文件描述符 文件描述符&#xff08;File Descriptor&#xff09;是一个非负整数&#xff0c;用于标识打开…

OJ习题之——圆括号编码

圆括号编码 1.题目描述2.完整代码3.图例演示 1.题目描述 题目描述 令Ss1 s2 …sn是一个规则的圆括号字符串。S以2种不同形式编码&#xff1a; &#xff08;1&#xff09;用一个整数序列Pp1 p2 … pn编码&#xff0c;pi代表在S中第i个右圆括号的左圆括号数量。&#xff08;记为…

C++——string(2)

5. string类非成员函数 上面的几个接口大家了解一下&#xff0c;下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的 操作&#xff0c;这里不一一列举&#xff0c;大家在需要用到时不明白了查文档即可。 试用rfind、substr、find、find_first_(not)_of void te…

WordPress供求插件API文档:用户登录

该文档为WordPress供求插件文档&#xff0c;详情请查看 WordPress供求插件&#xff1a;一款专注于同城生活信息发布的插件-CSDN博客文章浏览阅读67次。WordPress供求插件&#xff1a;sliver-urban-life 是一款专注于提供同城生活信息发布与查看的插件&#xff0c;该插件可以实…

Java中super关键字作用及解析

在 Java 中&#xff0c;super关键字主要有以下作用&#xff1a; 在子类构造方法中调用父类的构造方法&#xff1a;使用super关键字可以在子类的构造方法中显式调用父类的构造方法&#xff0c;以便继承父类的属性和行为。语法如下&#xff1a;这样可以确保父类的构造方法被正确…

Sora:AI视频生成的新机遇与挑战

随着科技的飞速进步&#xff0c;人工智能&#xff08;AI&#xff09;和机器学习&#xff08;ML&#xff09;技术已经深入渗透到社会的各个领域。其中&#xff0c;Sora这类基于AI的视频生成工具因其高度逼真的生成能力而备受瞩目。然而&#xff0c;正如一枚硬币有两面&#xff0…

Vue+Vue CLI学习

1、Vue基础 1.1、Vue简介 &#xff08;1&#xff09;Javascript框架 &#xff08;2&#xff09;简化Dom操作 &#xff08;3&#xff09;响应式数据驱动 vue基础&#xff1b;vue-cli;vue-router;vuex;element-ui;vue3 vue文件包括html、css、js 1.2、第一个Vue程序 Vue …

python异常机制

当代码出现异常后底下代码都不会被执行了&#xff0c;也就是程序崩溃了。当然能避免异常的话尽量避免但是有的时候这个是没有办法避免的。 异常处理 &#xff08;注&#xff1a;异常处理是从上往下处理&#xff0c;所以编写代码时要注意&#xff09; 语法 try:可能出现异常…

ospf虚链路实验简述

1、ospf虚链路实验简述 ospf虚链路配置 为解决普通区域不在骨干区域旁&#xff0c;通过配置Vlink-peer实现不同区域网络设备之间建立逻辑上的连接。 实验拓扑图 r1: sys sysname r1 undo info enable int loopb 0 ip add 1.1.1.1 32 ip add 200.200.200.200 32 quit int e0/0/…

(sub)三次握手四次挥手

TCP的三次握手与四次挥手理解及面试题 序列号seq&#xff1a;占4个字节&#xff0c;用来标记数据段的顺序&#xff0c;TCP把连接中发送的所有数据字节都编上一个序号&#xff0c;第一个字节的编号由本地随机产生&#xff1b;给字节编上序号后&#xff0c;就给每一个报文段指派一…

男人的玩具系统wordpress外贸网站主题模板

垂钓用品wordpress外贸模板 鱼饵、鱼竿、支架、钓箱、渔线轮、鱼竿等垂钓用品wordpress外贸模板。 https://www.jianzhanpress.com/?p3973 身体清洁wordpress外贸网站模板 浴盐、防蚊液、足部护理、沐浴液、洗手液、泡澡用品wordpress外贸网站模板。 https://www.jianzhan…

Spring揭秘:BeanDefinitionRegistry应用场景及实现原理!

内容概要 BeanDefinitionRegistry接口提供了灵活且强大的Bean定义管理能力&#xff0c;通过该接口&#xff0c;开发者可以动态地注册、检索和移除Bean定义&#xff0c;使得Spring容器在应对复杂应用场景时更加游刃有余&#xff0c;增强了Spring容器的可扩展性和动态性&#xf…

鸿蒙ArkTS语言快速入门-TS(一)

ArkTS与TS的学习 ArkTS与TS的关系简述TypeScript&#xff08;TS&#xff09;简述基础类型1&#xff0c;let2&#xff0c;const3&#xff0c;布尔类型4&#xff0c;数字number5&#xff0c;字符串string6&#xff0c;数组Array7&#xff0c;元组 Tuple8&#xff0c;枚举 enum9&a…

【设计模式 05】原型模式

有的时候&#xff0c;我们创建对象&#xff0c;需要耗费大量时间在一些资源型操作上&#xff0c;这个时候&#xff0c;我们就可以先创建出一个模板&#xff0c;然后每次创建的时候直接从模板复制即可&#xff0c;不用反复进行耗时的资源型操作。 python代码&#xff1a; impo…

Vue-Router使用

1.安装 npm install vue-router4 2. 添加路由 新建router文件夹&#xff0c;新建文件 index.ts import { createRouter, createWebHashHistory,createWebHistory} from "vue-router";const routes [{path: /login,component: () > import("../views/Logi…

18个惊艳的可视化大屏(第19辑):工业制造、智能工厂

实时监控和数据展示 可视化大屏可以集成和展示各种传感器、设备和系统的实时数据。通过将数据可视化展示在大屏上&#xff0c;工厂管理人员可以实时监控生产线的状态、设备的运行情况、生产效率等重要指标。这有助于及时发现问题、做出决策&#xff0c;并提高生产效率和质量。…

粉色ui微信小程序源码/背景图/头像/壁纸小程序源码带流量主

云开发版粉色UI微信小程序源码&#xff0c;背景图、头像、壁纸小程序源码&#xff0c;带流量主功能。 云开发小程序源码无需服务器和域名即可搭建小程序另外还带有流量主功能噢&#xff01;微信平台注册小程序就可以了。 这套粉色UI非常的好看&#xff0c;里面保护有背景图、…