JAVA每日面经——并发编程(二)必看

👩🏽‍💻个人主页:阿木木AEcru

🔥 系列专栏:《Docker容器化部署系列》 《Java每日面筋》

💹每一次技术突破,都是对自我能力的挑战和超越。

目录

一、什么是线程?什么是多线程?

二、 基础创建线程的方式

三、线程池

1.1 什么是线程池?

1.2 线程池有哪些核心参数?

1.3 常见使用的任务队列有哪些?

1.4 线程池中常用的拒绝策略。

四、线程池的回收

五、结尾

一、什么是线程?什么是多线程?

线程是程序执行流的最小单元,而多线程则是指在同一个程序中,可以同时运行多个线程,每个线程独立执行不同的任务。在多线程编程中,每个线程都拥有自己的执行栈和程序计数器,但它们共享程序的内存空间和其他资源。多线程可以提高程序的并发性和性能,允许程序在同时执行多个任务的情况下更有效地利用计算资源。

二、 基础创建线程的方式

创建线程的方式最常见的有继承Thread类、实现Runnable接口、实现Callable接口等。

实现Runnable接口和实现Callable接口的区别在于一个没有返回值,一个有返回值。

↓↓↓↓下面有示例,大家可以看看↓↓↓↓↓

  • 使用继承Thread类的方式创建线程:
arduino 复制代码
// 继承Thread类,重写run()方法
class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
        for (int i = 1; i <= 5; i++) {
            System.out.println("线程通过继承Thread类方式执行,当前数字:" + i);
            try {
                Thread.sleep(1000); // 暂停1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Threadtest {
    public static void main(String[] args) {
        MyThread thread = new MyThread(); // 创建线程对象
        thread.start(); // 启动线程
    }
}
  • 使用实现Runnable接口的方式创建线程:
java 复制代码
// 实现Runnable接口,重写run()方法
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的任务
        for (int i = 1; i <= 5; i++) {
            System.out.println("线程通过实现Runnable接口方式执行,当前数字:" + i);
            try {
                Thread.sleep(1000); // 暂停1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Threadtest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable(); // 创建实现了Runnable接口的对象
        Thread thread = new Thread(runnable); // 创建线程对象,传入Runnable对象
        thread.start(); // 启动线程
    }
}
 
  • 使用实现Callable的方式创建线程
java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 实现Callable接口,指定返回类型为Integer
class SumCalculator implements Callable<Integer> {
    public Integer call() {
        int sum = 0;
        // 计算1到100的和
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

public class Threadtest {
    public static void main(String[] args) {
        // 创建Callable对象
        Callable<Integer> calculator = new SumCalculator();

        // 创建FutureTask对象,传入Callable对象
        FutureTask<Integer> futureTask = new FutureTask<>(calculator);

        // 创建线程对象,传入FutureTask对象
        Thread thread = new Thread(futureTask);

        // 启动线程
        thread.start();

        try {
            // 获取线程执行的结果
            int result = futureTask.get();
            System.out.println("1到100的和为:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 使用匿名内部类的方式创建线程:
csharp 复制代码
public class Threadtest {
    public static void main(String[] args) {
        // 使用匿名内部类创建线程对象,并重写run()方法
        Thread thread = new Thread() {
            public void run() {
                // 线程执行的任务
                for (int i = 1; i <= 5; i++) {
                    System.out.println("线程通过匿名内部类方式执行,当前数字:" + i);
                    try {
                        Thread.sleep(1000); // 暂停1秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start(); // 启动线程
    }
}
  • 使用Lambda表达式的方式创建线程:
arduino 复制代码
public class Threadtest {
    public static void main(String[] args) {
        // 使用Lambda表达式创建线程对象,定义线程执行的任务
        Thread thread = new Thread(() -> {
            // 线程执行的任务
            for (int i = 1; i <= 5; i++) {
                System.out.println("线程通过Lambda表达式方式执行,当前数字:" + i);
                try {
                    Thread.sleep(1000); // 暂停1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start(); // 启动线程
    }
} 

三、线程池

除了上面这些最基础的方式,还有一种在实际开发中最常用的创建线程方式,那就使用线程池的方式。我们接着往下看。

1.1 什么是线程池?

线程池(Thread Pool)是一种在程序中管理线程的高级技术,它用于创建和管理线程的集合。线程池中的线程可以被重复利用,从而避免了频繁创建和销毁线程所带来的性能开销。在Java中,线程池通常通过ExecutorService接口及其实现类(如ThreadPoolExecutor)来实现。

工作原理:

线程创建:当提交一个任务(通常是一个实现了Runnable或Callable接口的对象)给线程池时,线程池会创建一个新的线程来执行这个任务。
任务执行:可以将已经写好的线程任务放到线程池中执行,提交之后就会去执行,或者放到等待队列中。
线程回收:任务完成后,线程不会立即被销毁,而是返回线程池中等待下一个任务。
任务队列:如果线程池中的所有线程都在忙碌状态,新提交的任务会被放入任务队列中等待执行。
线程复用:当线程池中的某个线程空闲时,它会从任务队列中取出下一个任务来执行,而不是创建新的线程。

好处:

提高性能:通过复用已创建的线程来减少线程创建和销毁的开销,这也是最大的好处,性能会更好。
控制并发级别:通过设置线程池的大小,可以控制并发执行任务的数量,防止执行的任务过多导致内存溢出。
增强稳定性:线程池可以设置线程的超时时间、拒绝策略等,使得在实际的业务中更加的灵活,也提高了系统的稳定性。

1.2 线程池有哪些核心参数?

核心线程数(Core Pool Size):线程池中始终保持活动状态的线程数目,即使它们处于空闲状态。当有任务提交到线程池时,线程池会优先使用核心线程来执行任务,直到达到核心线程数为止。核心线程数通常是根据系统资源和预期负载来确定的。
最大线程数(Maximum Pool Size):线程池中允许存在的最大线程数目,当任务数量超过核心线程数并且任务队列已满时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。超过最大线程数的任务会被拒绝执行,或者根据拒绝策略进行处理。
线程存活时间(Keep Alive Time):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间,即空闲线程在没有任务可执行时保持存活的时间。超过存活时间的空闲线程会被终止并从线程池中移除,以减少资源消耗。
任务队列(Work Queue):用于存储提交到线程池但尚未执行的任务的队列。当线程池中的线程数量达到核心线程数时,并且有新任务提交时,新任务会被放入任务队列中等待执行。不同类型的线程池可以使用不同的任务队列实现,如有界队列、无界队列、同步队列等。
拒绝策略(Rejected Execution Handler):当线程池无法接受新任务时的处理策略。常见的拒绝策略包括抛出异常、丢弃任务、丢弃最旧的任务、调用者运行等。

1.3 常见使用的任务队列有哪些?

1、LinkedBlockingDeque(链表同步阻塞队列):

● 作用:它作为一个双向链表的阻塞队列,在线程池中扮演着存储任务的角色。它既可以作为生产者和消费者之间的数据缓冲区,也可以作为任务的临时存储空间,保证任务的顺序执行。

● 任务进出逻辑:当线程池的工作线程就绪时,它会从队列中取出任务执行。如果队列为空,工作线程会被阻塞直到有新任务加入队列。当有新任务提交给线程池时,线程池会将任务放入队列的尾部,保证了任务的先进先出顺序。
2、ArrayBlockingQueue(数组同步阻塞队列):

● 作用:它是一个基于数组的有界阻塞队列,在线程池中用于存储任务。它控制了线程池中任务的数量,避免了任务过多导致内存溢出或性能下降的问题。

● 任务进出逻辑:当有新任务提交给线程池时,线程池会将任务放入队列中。如果队列已满,则新任务的插入操作会被阻塞,直到有工作线程取走队列中的任务为止。工作线程会从队列的头部取出任务执行,保证了任务的顺序性。
3、SynchronousQueue(同步阻塞队列):

● 作用:它是一个无缓冲的阻塞队列,用于在线程池中实现任务的直接传递。它不存储任务,而是将任务直接传递给工作线程,避免了任务缓存的开销。

● 任务进出逻辑:当有新任务提交给线程池时,线程池会尝试将任务直接传递给工作线程,如果所有工作线程都忙碌,则新任务的插入操作会被阻塞,直到有工作线程可用。这种机制实现了一种手递手的方式来传递任务,确保了任务的及时执行。

1.4 线程池中常用的拒绝策略。

拒绝策略是在线程池中,当任务无法被执行时,例如线程池已满或者达到最大任务队列容量,就会触发拒绝策略来处理这些无法执行的任务。

1、AbortPolicy(默认):这是默认的拒绝策略,当任务无法被执行时,会抛RejectedExecutionException异常。

例如:当订单量超过系统处理能力时,为了防止系统被过载,订单提交线程池可以使用这个拒绝策略,直接拒绝新的订单提交,并提示用户稍后再试。当然这也只是例子,实际业务还需要采取实际的处理方案。
2、CallerRunsPolicy:在这种策略下,线程池会将无法执行的任务交给提交任务的线程来执行。这意味着任务提交者会执行一部分任务,从而降低了提交速度,但能够保证任务不会被丢弃。

例如:在一个在线音乐平台的音乐推荐系统中,当用户点击某首热门歌曲时,系统会尝试推荐相关的歌曲给用户。如果推荐任务已满,无法立即执行,此时就会让用户请求的当前线程来执行推荐任务,从而保证推荐不会被丢弃。
3、DiscardPolicy:这种策略下,当任务无法被执行时,会被简单地丢弃,不提供任何反馈。这意味着有可能会丢失一些任务,不建议在需要任务完整执行的场景下使用。

例如:在一些系统的后台操作日志的异步处理时,如果说突然有大量的日志需要写入,此时队列中无法存下这么多任务时,使用此拒绝策略,就会抛弃掉一些日志的处理,当然前提是这些日志不是特别的重要。
4、DiscardOldestPolicy:如果线程池未关闭,并且任务队列已满,则丢弃队列中最末尾的一个任务,并将新任务添加到队列中。这种策略在一定程度上保留了任务执行的机会,同时也有可能丢失一些旧的任务。

例如:在视频推荐系统中,当用户在观看视频时,系统会尝试推荐相关视频给用户。如果推荐任务队列已满,无法立即执行,则会丢弃队列中最末尾的推荐任务,并将新的推荐任务添加到队列中,以确保用户能够及时获取到相关推荐。

当然,上面的这些例子是为了讲述相关拒绝策略的区别。实际业务需要使用合理的队列以及拒绝策略进行搭配完成。

下面是一个简单的实现例子:

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 定义线程池参数
        int corePoolSize = 5; // 核心线程数
        int maximumPoolSize = 10; // 最大线程数
        long keepAliveTime = 120; // 线程存活时间(秒)
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 任务队列
        RejectedExecutionHandler rejectionHandler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS,
                workQueue, rejectionHandler);

        // 提交任务
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                //在这里还可以做一些延时操作,来测试存活时间,以及拒绝策略等
                //打印任务信息
                System.out.println("执行任务:" + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

这里使用ThreadPoolExecutor类创建了一个线程池,并通过构造函数设置了核心线程数、最大线程数、线程存活时间、任务队列和拒绝策略。然后向线程池提交了10个任务,每个任务都会打印当前线程的名称。最后调用shutdown方法关闭线程池。

四、线程池的回收

在线程池中,我们有核心线程和非核心线程两类工作人员。 核心线程是一直待命在线程池中的工作人员,可以通过两种方式来准备好迎接任务:

1、当我们向线程池添加任务时,核心线程会被动地准备好。

2、我们也可以主动调用 prestartAllCoreThreads 方法来预先启动所有核心线程。

当线程池中的任务队列满了,为了增加线程池的处理能力,我们会临时招募一些非核心线程。 核心线程和非核心线程的数量是在创建线程池时就确定的,但也可以在运行时进行调整。

由于非核心线程只是临时帮衬的,所以当它们处理完任务后,就会进入空闲状态,等待回收。 线程池中的所有工作人员都是从任务队列中获取任务来执行的。如果在一段时间内,任务队列都没有任务可供处理,那么这个线程就可以被回收了。 这个回收功能是通过检查阻塞队列里的 poll 方法来实现的。当超过指定的时间没有获取到任务时,poll 方法会返回 null,这时候当前线程就可以被回收了。

默认情况下,线程池只会回收非核心线程。如果需要连核心线程也一并回收,我们可以设置一个叫做 allowCoreThreadTimeOut 的属性为 true。通常情况下,我们不会去回收核心线程,因为它们本身就是实现线程的复用,而且在没有任务的时候会处于阻塞状态,不会占用 CPU 资源。

五、结尾

感谢您的观看! 如果本文对您有帮助,麻烦用您发财的小手点个三连吧!您的支持就是作者前进的最大动力!再次感谢!

相关推荐
侠客行03176 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪6 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚8 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎9 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码9 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚9 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂9 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang9 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐9 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG9 小时前
JavaTuples 库分析
java