【程序员快速复习系列】Java多线程(1-基础知识、线程创建)

0. 前言

程序员面试本是一件再平常不过的事情,记得刚毕业的时候面试题背的滚瓜烂熟。但是在职程序员面试却是另一回事了,我们往往没有太多时间复习,特别是大龄程序员,工作日忙于工作,周末还要照顾家庭,一旦面临被优化的风险就很被动,难以在短时间内复习并找到工作。不要问我是怎么知道的,都是切身体会,在复习的过程中我也走了不少弯路,所幸最终结果令自己满意。
为了不让和我一样的程序员遇到同样的问题,我打算写这一系列的文章,这些文章不会像其他面经一般大而全,这些文章仅记录我在复习过程中认为重要的知识点,如果能帮助到你就太好了。

1. 线程的定义、和进程的区别、线程的生命周期

一句话回答:线程是进程中独立运行的最小单位,共享进程的资源,而进程是操作系统分配资源的最小单位,拥有独立的内存空间。

细节解释:

1.1 定义

线程是进程中独立运行的最小单位,它拥有自己的堆栈、程序计数器和局部变量,但与进程共享相同的内存空间、打开的文件和全局变量。

1.2 进程和线程的区别

  • 资源分配:进程是操作系统分配资源的最小单位,拥有独立的内存空间、打开的文件和全局变量。线程则是进程中独立运行的最小单位,共享进程的资源。
  • 执行方式:进程可以独立执行,而线程必须依附于进程才能执行。
  • 创建开销:创建进程的开销比创建线程的开销大。
  • 通信方式:进程间通信需要使用IPC(进程间通信)机制,而线程间通信可以直接访问共享内存。

1.3 线程的生命周期

  • 新建 (New):线程被创建但尚未启动。
  • 就绪 (Runnable):线程已准备就绪,等待 CPU 资源。
  • 运行 (Running):线程正在执行。
  • 阻塞 (Blocked):线程被阻塞,等待某个事件发生,例如 I/O 操作完成或锁释放。
  • 死亡 (Dead):线程执行完毕或遇到异常而终止。

线程是进程中更轻量级的执行单元,它们共享进程的资源,但拥有独立的执行路径。理解线程和进程的区别以及线程的生命周期,对于编写高效、可靠的多线程程序至关重要。

2. 在Java中创建线程有哪些方式

一句话回答:Java中创建线程的方式主要有三种:继承Thread类、实现Runnable接口、使用Callable和Future。

细节解释:

2.1 继承Thread类

  • 创建一个继承自Thread的类,并重写run方法。
  • 示例代码:
    class MyThread extends Thread {
        public void run() {
            System.out.println("MyThread is running.");
        }
    }
    // 使用
    MyThread myThread = new MyThread();
    myThread.start();
    

2.2 实现Runnable接口

  • 创建一个实现Runnable接口的类,实现run方法。
  • 示例代码:
    class MyRunnable implements Runnable {
        public void run() {
            System.out.println("MyRunnable is running.");
        }
    }
    // 使用
    Thread thread = new Thread(new MyRunnable());
    thread.start();
    

2.3 使用Callable和Future

  • 创建一个实现Callable接口的类,实现call方法,并返回值。
  • 使用FutureTask包装Callable任务。
  • 示例代码:
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    class MyCallable implements Callable<Integer> {
        public Integer call() {
            System.out.println("MyCallable is running.");
            return 123;
        }
    }
    // 使用
    FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
    Thread thread = new Thread(futureTask);
    thread.start();
    // 获取结果
    try {
        Integer result = futureTask.get(); // 阻塞直到任务完成
        System.out.println("Result: " + result);
    } catch (Exception e) {
        e.printStackTrace();
    }
    

2.4 使用Executor框架

  • 使用Executors创建线程池,并使用ExecutorService提交任务。
  • 示例代码:
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    // 创建固定大小的线程池
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 提交任务
    executorService.submit(new MyRunnable());
    // 关闭线程池
    executorService.shutdown();
    

2.5 ThreadPoolExecutor

  • 使用ThreadPoolExecutor创建线程池提供了高度的自定义性,允许开发者根据应用需求精确配置线程池的行为。
  • 示例代码:
import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 线程工厂,用于创建具有名称前缀的线程,方便识别
        ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNameFormat("custom-pool-%d").build();

        // 饱和策略,定义当任务太多来不及处理时的行为
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 创建线程池
        int corePoolSize = 5; // 核心线程数
        int maximumPoolSize = 10; // 最大线程数
        long keepAliveTime = 1; // 非核心线程空闲存活时间
        TimeUnit unit = TimeUnit.MINUTES; // 存活时间单位
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列

        ExecutorService executorService = new ThreadPoolExecutor(
                corePoolSize, 
                maximumPoolSize, 
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        // 提交任务到线程池
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executorService.submit(() -> {
                System.out.println("执行任务: " + finalI + " 由线程 " + Thread.currentThread().getName() + " 处理");
            });
        }

        // 关闭线程池,不再接受新任务,已提交的任务将执行完毕
        executorService.shutdown();

        try {
            // 等待线程池关闭,即所有任务执行完毕
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                // 超时后强制关闭线程池
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            // 当前线程被中断时的处理
            executorService.shutdownNow();
        }
    }
}

3. 为什么阿里不建议使用Executors类的静态方法创建线程

一句话回答:阿里巴巴推荐使用ThreadPoolExecutor而非Executors类创建线程池,是因为前者提供了更精细的控制和自定义配置,有助于防止资源耗尽并增强线程池的健壮性。

细节解释:

  • 细粒度控制ThreadPoolExecutor允许开发者设置核心线程数、最大线程数、存活时间、工作队列等参数,实现对线程池行为的精确控制。
  • 自定义线程工厂:通过实现ThreadFactory接口,可以创建具有统一命名的线程,便于问题追踪和日志记录。
  • 参数合理配置:合理配置线程池参数,平衡资源使用和系统性能,避免资源浪费或系统过载。
  • 灵活的拒绝策略RejectedExecutionHandler提供多种任务拒绝策略,适应不同应用场景,增强线程池的健壮性。
  • 避免资源耗尽Executors类可能因缺乏有效资源限制和拒绝策略,在高并发下导致资源过度消耗。
  • 线程池生命周期管理ExecutorService接口提供灵活的线程池生命周期管理,包括任务提交和关闭操作。
  • 资源合理释放:通过shutdownshutdownNow方法,确保线程池在不再需要时及时释放资源,防止内存泄漏。
  • 避免使用Executors的缺点Executors类可能使用无界队列或不适当默认参数,高负载下可能导致线程数量失控。
  • 提高线程池的可维护性:自定义配置和细粒度控制使得ThreadPoolExecutor创建的线程池更易于监控和维护,提升应用程序稳定性。

4. 线程池的核心参数有哪些

一句话回答:线程池的核心参数包括线程池初始大小、最大线程数、空闲线程存活时间、时间单位、工作队列和拒绝策略,它们共同决定了线程池如何有效管理和执行任务。

细节解释:

  • 线程池初始大小(corePoolSize):线程池中始终保持的线程数量,即使线程空闲也不会被回收。
  • 最大线程数(maximumPoolSize):线程池中允许的最大线程数量,超过这个数量的新任务将被放入工作队列或根据拒绝策略处理。
  • 空闲线程存活时间(keepAliveTime):设置当线程数超过初始大小时,多余的空闲线程在被终止前等待新任务的最长时间。
  • 时间单位(unit):与空闲线程存活时间配合使用,定义了存活时间的时间单位,如秒、毫秒等。
  • 工作队列(workQueue):用于存放待处理任务的阻塞队列,当线程池中的线程全忙时,新提交的任务将入队等待。
  • 拒绝策略(handler):当工作队列满了且线程池中的线程数达到最大线程数时,线程池将根据设定的拒绝策略来处理新提交的任务。

Java线程池的默认拒绝策略是AbortPolicy,它会抛出一个RejectedExecutionException异常,同时还有其他几种预定义的拒绝策略可供选择。

  • AbortPolicy:默认策略,当任务被拒绝时会抛出RejectedExecutionException异常,并且会打印一条日志信息。
  • CallerRunsPolicy:这个策略将任务的执行权交给调用execute(Runnable)方法的线程,即提交任务的线程会尝试自己运行这个任务。
  • DiscardPolicy:这个策略会直接丢弃被拒绝的任务,不抛出异常,也不执行任务。
  • DiscardOldestPolicy:这个策略会丢弃工作队列中等待时间最长的任务,然后尝试重新提交当前任务。

除了这些预定义的拒绝策略外,开发者还可以通过实现RejectedExecutionHandler接口来定义自己的拒绝策略,以便更灵活地处理任务拒绝的情况。自定义拒绝策略可以包括记录日志、发送通知、动态增加线程资源等操作。

5. 线程池设置多大合适

一句话回答:线程池大小的选择策略应基于任务特性、系统资源和并发需求,并通过实际测试调整以找到最优解。

细节解释:

  • 任务特性:CPU密集型任务的线程池大小应接近处理器核心数,而I/O密集型任务可以适当增加线程数以提高资源利用率。
  • 系统资源:考虑服务器的CPU核心数和内存限制,避免线程过多导致资源竞争和上下文切换。
  • 并发需求:根据系统的最大并发任务数预估线程池大小,确保在高并发情况下系统能够稳定响应。
  • 经验公式:常用的经验公式为Nthreads = Ncpu * (Ucpu + W/C),其中Ncpu是CPU核心数,Ucpu是CPU使用率,W是任务等待时间,C是任务计算时间。
  • 性能测试:通过实际的性能测试来评估不同线程池大小对系统性能的影响,并据此调整大小。
  • 自适应调整:在某些场景下,可以采用动态调整策略,根据实时监控数据自动调整线程池大小。

6. CompletableFuture是什么

一句话回答:CompletableFuture 是 Java 8 中引入的用于异步编程的类,它提供了一种编写非阻塞代码的方式,允许以声明式的风格处理异步操作。

细节解释:
CompletableFuture 支持以同步或异步的方式表示一个未来某个时刻完成的操作,并且可以对其进行链式调用,实现复杂的异步逻辑。

代码示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 模拟耗时操作
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "完成";
});

future.thenAccept(result -> {
    System.out.println(result); // 处理结果
}).join(); // 等待异步操作完成

// 可以链式添加更多的操作
future.thenApply(s -> s.toUpperCase())
      .thenAccept(upperCaseResult -> System.out.println(upperCaseResult));

在这个例子中,supplyAsync 方法异步地执行了一个耗时的操作,并返回了一个 CompletableFuture 对象。thenAccept 方法添加了一个回调,用于在异步操作完成时处理结果。join 方法用于等待异步操作完成。链式调用 thenApply 对结果进行了进一步的处理,并且 thenAccept 用于输出处理后的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值