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 资源。

五、结尾

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

相关推荐
XiaoLeisj26 分钟前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck28 分钟前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei28 分钟前
java的类加载机制的学习
java·学习
Yaml42 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~2 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616882 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7893 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java3 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~3 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
程序媛小果4 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot