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

news2024/10/24 10:28:05

创建线程的四种方式

创建线程的四种方式包括使用继承 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 框架:
    • 优点:
      • 提供了高度灵活的线程管理、任务调度和线程池功能。
      • 可以降低线程创建和销毁的开销,提高性能。
      • 可以通过统一的接口来控制和管理多线程任务。
    • 缺点:
      • 可能因为线程池参数配置不合理而导致资源浪费或性能下降。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图形系统开发实战课程:进阶篇(上)——10.应用实例:交通路网

图形开发学院&#xff5c;GraphAnyWhere 课程名称&#xff1a;图形系统开发实战课程&#xff1a;进阶篇(上)课程章节&#xff1a;“图形样式”原文地址&#xff1a;https://www.graphanywhere.com/graph/advanced/2-10.html 第十章 应用实例&#xff1a;交通路网 \quad 在前面几…

Spring学习 基础(一)

Spring基础 IoC容器&#xff08;Inversion of Control&#xff09;: Spring 的核心是其控制反转&#xff08;IoC&#xff09;容器&#xff0c;它负责管理对象的生命周期和相互之间的依赖关系。通过依赖注入&#xff08;Dependency Injection&#xff09;&#xff0c;Spring能够…

python 基础知识点(蓝桥杯python科目个人复习计划59)

今日复习内容&#xff1a;做题 例题1&#xff1a;建造房屋 问题描述&#xff1a; 小蓝和小桥是两位年轻的建筑师&#xff0c;他们正在设计一座新的城市。 在这个城市中&#xff0c;有N条街道&#xff0c;每条街道上有M个位置可以建造房屋&#xff08;一个位置只能建造一个房…

【java】后序遍历二叉树

采用递归方式实现 节点类 public class Node {private int value;//父节点private Node fNode;//左节点private Node left;//右节点private Node right;//是否已经打印过private boolean sign false;public Node() {}public boolean isSign() {return sign;}public void setS…

融资项目——OpenFeign的降级与熔断

当一个微服务调用其他微服务时&#xff0c;如果被调用的微服务因各种原因无法在规定时间内提供服务&#xff0c;则可以直接使用本地的服务作为备选&#xff0c;即进行降级熔断。 如之前所提到的微服务为例&#xff1a; 如果希望实现降级熔断&#xff0c;可以在本地创建一个实现…

【Spring云原生系列】Spring Cloud Stream:消息驱动架构(MDA)解析,实现异步处理与解耦合!

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

HYBBS 表白墙网站PHP程序源码

安装教程 上传程序安装&#xff0c;然后设置账号密码&#xff0c;登陆后台切换模板手机PC都要换开启插件访问前台。 安装完成后如果不能正常访问就删除install安装文件夹 安装完成后右上角登录后点击头图进入后台 找到插件 安装表白墙配置插件 找到模板 将表白墙模板同时设…

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

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

多模态入门

VIT处理图像 CNN VS Transformer 多模态BLIP模型 网络结构 视觉编码器: 就是 ViT 的架构。将输入图像分割成一个个的 Patch 并将它们编码为一系列 Image Embedding,并使用额外的 [CLS] token 来表示全局的图像特征。视觉编码器不采用之前的基于目标检测器的形式,因为 ViLT 和…

【Linux】权限管理(文件的访问者、类型和访问权限,chmod、chown、chgrp、umask,粘滞位)

目录 00.前言 01.文件访问者的分类 02.文件类型和访问权限 文件类型&#xff1a; 文件基本权限&#xff1a; 03.文件权限值的表示方法 04.访问权限的设置 &#xff08;1&#xff09;chmod &#xff08;2&#xff09;chown &#xff08;3&#xff09;chgrp &#xff0…

Redis安全加固策略:绑定Redis监听的IP地址 修改默认端口 禁用或者重命名高危命令

Redis安全加固策略&#xff1a;绑定Redis监听的IP地址 & 修改默认端口 & 禁用或者重命名高危命令 1.1 绑定Redis监听的IP地址1.2 修改默认端口1.3 禁用或者重命名高危命令1.4 附&#xff1a;redis配置文件详解&#xff08;来源于网络&#xff09; &#x1f496;The Beg…

启动Docker镜像时候,ENTRYPOINT 和CMD这两者指令的写法有什么不同和区别?

ENTRYPOINT和CMD在Dockerfile中都用于指定容器启动时执行的命令&#xff0c;但它们之间存在一些关键的区别和不同的用途&#xff1a; 1. 基本用途和行为差异 ENTRYPOINT 定义了容器启动时执行的基础命令&#xff0c;使得容器像一个可执行程序。ENTRYPOINT让你能够指定容器启动…

Pycharm+Selenium WebdriverPython自动化测试

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

JAVA回顾总结--IO和装饰模式

目录 一、IO流的分类流的体系结构 二、IO流的基础使用2.1 FileReader和FileWriter2.2 FileInputStream和FileOuputStream2.3 缓冲流 Buffered2.4 转换流 InputStreamReader和OutputStreamWriter2.5 对象流 ObjectInputStream 和 ObjectOutputStream 三、IO结构和装饰模式3.1 装…

【软考】设计模式之访问者模式

目录 1. 说明2. 应用场景3. 结构图4. 构成5. java示例5.1 喂动物5.1.1 抽象访问者5.1.2 具体访问者5.1.3 抽象元素5.1.4 具体元素5.1.5 对象结构5.1.6 客户端类5.1.7 结果示例 5.2 超市销售系统5.2.1 业务场景5.2.2 业务需求类图5.2.3 抽象访问者5.2.4 具体访问者5.2.5 抽象元素…

SAR ADC学习笔记(4)

CDAC电容阵列 一、电容失配 二、电容失配对CDAC线性度的影响 1.电容失配对DNL的影响 2.电容失配对INL的影响 三、分段结构的CDAC 四、CDAC开关切换方案&#xff1a;传统开关切换策略 第一次比较阶段&#xff1a;如果VP(1)-VN(1)<0 第一次比较阶段&#xff1a;如果VP(1)-VN…

【Godot4.2】GDScript数组分类及类型化数组和紧缩数组概述

概述 GDScript的数组是一种很常用的数据类型。本文主要阐述一下GDScript数组分类&#xff0c;以及官方文档和大多数视频或教程较少提及的类型化数组和紧缩数组。 GDScript数组分类 通过反复查阅GDScript内置文档并进行细节比较&#xff0c;发现GDScript的数组&#xff0c;可…

LVS集群(Linux Virtual server)

集群概念lvs模型lvs调度算法lvs实现lvs高可用性&#xff0c;负载均衡 1 集群和分布式 系统性能扩展方式&#xff1a; Scale UP&#xff1a;垂直扩展&#xff0c;向上扩展,增强&#xff0c;性能更强的计算机运行同样的服务 升级单机的硬件设备Scale Out&#xff1a;水平扩展…

【DP】蓝桥杯第十三届-费用报销

#include<iostream> #include<algorithm> #include<cstring> #include<set> #include<queue> using namespace std; const int N1010; int dp[N][5010];//dp[i][j]:选到第i个物品是否能取到价值j&#xff1b; int month[13]{0,31,28,31,30,31,30…

Unity3D学习之XLua实践——背包系统

文章目录 1 前言2 新建工程导入必要资源2.1 AB包设置2.2 C# 脚本2.3 VSCode 的环境搭建 3 面板拼凑3.1 主面板拼凑3.2 背包面板拼凑3.3 格子复合组件拼凑3.4 常用类别名准备3.5 数据准备3.5.1 图集准备3.5.2 json3.5.3 打AB包 4 Lua读取json表及准备玩家数据5 主面板逻辑6 背包…