Java 并发编程面试题——创建线程

目录

  • 1.创建线程的方式有哪几种?
    • 1.1.继承 Thread 类,并重写 run 方法
    • 1.2.实现 Runnable 接口中的 run 方法
    • 1.3.实现 Callable 接口的 call() 方法,并结合来 Future 实现
    • 1.4.通过线程池创建
  • 2.上述创建线程的方式有什么优缺点?
  • 3.Runnable 和 Callable 有什么区别?

1.创建线程的方式有哪几种?

1.1.继承 Thread 类,并重写 run 方法

(1)具体步骤如下:

  • 定义类 MyThread 来继承 Thread 类,重写 run 方法;
  • 然后创建该类的对象,并调用 start() 方法启动线程。
public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }
    
    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

(2)注意要调用 start() 方法后,该线程才算启动!我们在程序里面调用了start() 方法后,虚拟机会先为我们创建⼀个线程,然后等到这个线程第⼀次得到时间片时再调用 run() 方法。 注意不可多次调用 start() 方法。在第一次调用start() 方法后,再次调用start() 方法会抛出异常。

(3)也可以直接使用 Thread 类来创建线程:

@Slf4j
public class Demo {
    public static void main(String[] args) {
    	//创建线程对象
        Thread t = new Thread(() -> {
        	//要执行的任务
            log.debug("running");
        });
        //为线程设置名称,也可以在创建线程时直接指定,例如 Thread t1 = new Thread("t1") {...};
        t.setName("t1");
        //启动线程
        t.start();
        
		log.debug("running");
        //注:上面两条日志打印语句的顺序不固定
    }
}

1.2.实现 Runnable 接口中的 run 方法

(1)具体步骤如下:

  • 定义一个类 MyThread 实现 Runnable 接口,并实现 run 方法;
  • 然后创建该类的对象,并将其作为 target 传入 Thread 的构造函数中,需要注意的是也可以使用 lambda 表达式来进行简化;
  • 最后调用 start() 方法启动线程。

Runnable 接口 (JDK 1.8 +) 代码如下:

@FunctionalInterface
public interface Runnable {
	public abstract void run(); 
}

可以看到 Runnable 是⼀个函数式接口,这意味着我们可以使用 Java 8 的函数式编程来简化代码。示例代码如下:

public class Demo {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }
    
    public static void main(String[] args) {
        new Thread(new MyThread()).start();
        
        //或者使用 Java 8 函数式编程,可以省略 MyThread 类
        new Thread(() -> {
            System.out.println("Java 8 匿名内部类");
        }).start();
    }
}

(2)也可以通过如下方式创建线程:

@Slf4j
public class Demo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        
        Thread t = new Thread(r, "t1");
        t.start();
        
        log.debug("running");
    }
}

Java 8 以后可以使用 lambda 精简代码(本代码中的前提是接口中只有一个抽象方法):

public class Demo {
    public static void main(String[] args) {
        Runnable r = () -> {
            log.debug("running");
        };
        Thread t = new Thread(r, "t1");
        t.start();
        log.debug("running");
    }
}

1.3.实现 Callable 接口的 call() 方法,并结合来 Future 实现

(1)具体步骤如下:

  • 定义一个 Callable 的实现类 Task,并实现 call() 方法(有返回值);
  • 将 Task 类对象作为参数传入到 FutureTask 的构造函数中;
  • 再把 FutureTask 作为 target 传入到 Thread 的构造函数中,创建 Thread 线程对象,并调用 start() 方法启动线程;
  • 最后通过 FutureTask 的 get 方法来异步获取线程的执行结果
class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要3秒
        Thread.sleep(3000);
        return 2;
    }
    
    public static void main(String args[]) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new Task());
        new Thread(task).start();
        Integer res = task.get();
        //注意调⽤ get ⽅法会阻塞当前线程,直到得到结果,所以实际编码中建议使⽤可以设置超时时间的重载 get ⽅法。
        System.out.println(res);
        System.out.println("end...");
    }
}

输出结果如下:

2
end...

(2)此外,Callable⼀般也可配合线程池工具 ExecutorService 来使用。这里只介绍 ExecutorService 可以使用 submit 方法来让⼀个 Callable 接口执行。它会返回⼀个 Future,我们后续的程序可以通过这个 Future 的 get 方法得到结果。这里可以看⼀个简单的使用案例:

import java.util.concurrent.*;

class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要⼀秒
        Thread.sleep(1000);
        return 2;
    }
    
    public static void main(String args[]) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        //注意调⽤ get ⽅法会阻塞当前线程,直到得到结果,所以实际编码中建议使⽤可以设置超时时间的重载 get ⽅法。
        System.out.println(result.get());
    }
}

输出结果同上。

1.4.通过线程池创建

使用 ThreadPoolExecutor 类的构造函数可以自定义线程池,下面使用参数最全的构造函数来举例:

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;

class Solution {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //自定义线程池
        int corePoolSize = 2;
        int maximumPoolSize = 5;
        long keepAliveTime = 50;
        // keepAliveTime 的单位
        TimeUnit unit = TimeUnit.MICROSECONDS;
        //工作队列 workQueue
        BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(3);
        //使用开源框架 guava 提供的 ThreadFactoryBuilder 可以给线程池里的线程自定义名字
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-task-%d").build();
        //饱和策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
        ThreadPoolExecutor threadsPool = new ThreadPoolExecutor(
                                             corePoolSize, maximumPoolSize,
                                             keepAliveTime, unit,
                                             blockingQueue, threadFactory,
                                             handler);
        //执行无返回值的任务
        Runnable taskWithoutRet = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " is running");
            }
        };
        threadsPool.execute(taskWithoutRet);
    
        //执行有返回值的任务
        FutureTask<Integer> taskWithRet = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + " is running");
                //线程睡眠 1000 ms
                Thread.sleep(1000);
                return 100;
            }
        });
        threadsPool.submit(taskWithRet);
        System.out.println("有返回值的任务的结果为: " + taskWithRet.get());
        
        //关闭线程池
        threadsPool.shutdown();
    }
}

① 有关 Futrue 的相关知识可以参考Java 并发编程面试题——Future这篇文章。
② 有关线程池的相关知识可以参考Java并发编程面试题——线程池这篇文章。

2.上述创建线程的方式有什么优缺点?

(1)使用继承 Thread 类的方式创建线程:

  • 优点:线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,体现了面向对象的思想
  • 缺点:编程稍微复杂一些,如果要访问当前线程,则必须使用 Thread.currentThread() 方法

(2)采用实现 Runnable、Callable 接口的方式创建线程:

  • 优点:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
  • 缺点:线程类已经继承了 Thread 类,所以不能再继承其他父类。

(3)通过线程池来创建线程:

  • 优点
    • 降低线程创建和销毁的开销:线程池中的线程可以被重复利用,避免了频繁创建和销毁线程的开销,从而提高了程序的性能。
    • 控制并发数:线程池可以限制并发执行的线程数量,避免系统资源被过度占用,从而提高了程序的稳定性。
    • 提高响应速度:线程池中已经存在的线程可以更快地响应任务请求,减少了线程创建和启动的时间,从而提高了程序的响应速度。
  • 缺点
    • 线程池本身需要占用一定的系统资源,当线程池中的线程数量过多时,会占用较多的内存和CPU资源。
    • 线程池中的任务队列可能会产生阻塞,当任务队列已满时,新的任务请求需要等待队列中的任务完成才能被处理,从而降低了程序的并发性能。
    • 如果线程池中的线程出现异常或者死循环等问题,可能会导致整个系统的崩溃,因此需要对线程池进行合理的配置和管理。

3.Runnable 和 Callable 有什么区别?

Runnable 和 Callable 都是 Java 并发编程中用于创建线程的接口,它们的主要区别如下:

  • 规定的方法:Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()
  • 返回值:Callable 中的 call() 方法返回一个泛型类型的结果,而 Runnable 的 run() 方法无返回值
  • 抛出异常:call() 方法可以抛出异常,run() 方法不可以
  • 使用场景:Callable 接口通常用于执行需要返回结果的复杂任务(异步获取结果),Runnable 接口通常用于执行简单任务。

你可能感兴趣的:(Java,后端面试,java,jvm,开发语言)