1.1.1 Java创建和启动线程的方式有哪些?它们之间有什么区别?

在Java中,创建和启动线程的方式主要有4种,分别为继承Thread类,实现Runnable接口,使用Callable和Future接口,使用线程池。

下面我们详细介绍这4种方式及其区别。

(1)继承Thread类。

当一个类继承自Thread类时,可以通过重写run()方法来定义线程执行的任务,然后通过创建该类的实例并调用start()方法来启动线程。代码如下。

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
    }
}
 
MyThread t = new MyThread();
t.start();

这种方式的优点是编码简单,能够直接使用;缺点是Java不支持多重继承,如果我们的类已经继承了另一个类,就不能使用这种方式创建线程。

(2)实现Runnable接口。

实现Runnable接口是创建线程的另一种方式。我们需要实现run()方法,然后将Runnable实例传递给Thread类的构造器,最后调用线程的start()方法。代码如下。

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务
    }
}
 
Thread t = new Thread(new MyRunnable());
t.start();

这种方式的优点是更灵活,允许我们的类继承其他类。同时,它也鼓励采用组合而非继承的设计原则,这使得代码更加灵活和易于维护。它的缺点是编程较复杂,需要构造Thread对象。

(3)使用Callable和Future接口。

Callable和Future接口是一种更灵活的线程机制。Future接口有几个方法可以控制关联的Callable任务对象。FutureTask实现了Future接口,通过它的get()方法可以获取Callable任务对象的返回值。代码如下。

FutureTask<Integer> futureTask=new FutureTask<Integer>(
     (Callable<Integer>)()-> {
           // 返回执行结果
           return 123;
     }
);
new Thread (futureTask,"返回值的线程").start();
try{
    // 使用get()来获取Callable任务对象的返回值
    System.out .println("Callable任务对象的返回值:"+futureTask.get());
}catch(Exception e) {
    e.printStackTrace();
}

相比于实现Runnable接口方式,使用Callable和Future接口可以返回执行结果,也能抛出经过检查的异常。这种方式更加灵活,适用于复杂的并发任务。它的缺点是相对复杂,get()方法在等待计算完成时是阻塞的。如果计算被延迟或永久挂起,调用者可能会长时间阻塞。

(4)使用线程池。

通过Executors的静态工厂方法获得ExecutorService实例,然后调用该实例的execute(Runnable command)方法即可使用线程池创建线程。一旦Runnable任务传递到execute()方法,该方法便会在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程便会创建一个新的线程来执行任务。示例代码如下。

public class Test4 {
   public static void main(String[] args) {
      ExecutorService executorService=Executors.newCachedThreadPool();
      for (int i = 0; i < 5; i++){
        executorService.execute(new MyTask());
        System.out.println("************* a"+i+"*************");
      }
      executorService.shutdown( );
   }
}
 
class MyTask implements Runnable{ public void run( ){ System.out.println(Thread.currentThread().getName()+"线程被调用了。"); } }

使用线程池方式的优点是能够自动管理线程的创建、执行和销毁,避免了创建大量线程引起的性能问题(因为频繁地创建和销毁线程会消耗大量系统资源),还能够限制系统中并发执行线程的数量,避免了大量并发线程消耗系统所有资源,导致系统崩溃。它的缺点是代码更为复杂,需要进行更多的设计和考虑,比如线程池的大小选择、任务的提交与执行策略等。如果线程池使用不当或没有正确关闭,可能会导致资源泄漏。

(5)4种方式的区别。

上述4种创建和启动线程的方式都有其适用场景和优缺点。

继承Thread类:简单直接,适用于简单的线程任务,不需要返回值,也不抛出异常,但在某些情况下因为Java的单继承限制而不够灵活。

实现Runnable接口:更加灵活,分离了线程的创建和任务的执行,符合面向对象的设计原则,适用于多个线程执行相同任务的场景,特别是当需要访问当前对象的成员变量和方法时。

使用Callable和Future接口:比实现Runnable接口复杂一些,使用也更复杂,但是提供了更强大的功能,适用于需要返回执行结果的多线程任务,或者需要处理线程中的异常的场景。

使用线程池:重用线程,减少创建和销毁线程的开销,并提供了控制最大并发线程数、调度、执行、监视、回收等一整套线程管理解决方案。

综上所述,每种方式都有其用武之地,开发者需要根据具体场景选择适合的创建和启动线程的方式。简单任务通常只需要使用Runnable接口或Thread类,而复杂的并发程序可能会需要使用Callable、Future接口和线程池来提供更高级的并发管理功能。