JAVA多线程(8.0)
目录
线程池
为什么使用线程池
线程池的使用
工厂类Executors(工厂模式)
submit
实现一个线程池
线程池
为什么使用线程池
在前面我们都是通过new Thread() 来创建线程的,虽然在java中对线程的创建、中断、销毁、等值等功能提供了支持,一个线程的创建和销毁虽然消耗虽然小,但从操作系统角度来看,如果我们频繁的创建和销毁线程,是需要大量的时间和资源的,那么有没有什么开销更小的方法?
第一种是协程,它可以说是轻量级线程,但是java很少用,多用于go和python。
第二种是线程池,java中多用线程池去解决频繁的创建和销毁线程问题。
那么为啥引入线程池就能够提升效率呢?
1.直接创建/销毁线程,是需要在用户态+内核态配合完成的工作,对于线程池,只需要在用户态即可,不需要内核态的配合,这样开销就更小
2.等线程用完之后,线程池不会销毁该线程,而是让其阻塞,等下次用的时候会再次利用它,所以不用频繁的进行创建和销毁。
线程池最核心的设计思路:复用线程,平摊线程的创建与销毁的开销代价
线程池的使用
java 提供了多种方式来创建线程池,主要通过
Executors(执行者)
工厂类或直接使ThreadPoolExecutor
类来完成
工厂类Executors(工厂模式)
使用Executors工厂类:
newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程数量由nThreads参数确定。
newCachedThreadPool():创建一个线程数量为动态的线程池,线程数量会根据任务数量动态变化,当长时间没有新任务时,空闲线程会被终止。newSingleThreadExecutor():创建一个单线程的线程池,它只会创建一个线程来执行任务。
newScheduledThreadPool(int corePoolSize):创建一个可以安排任务的线程池,可以指定延迟执行任务或定期执行任务。后面两个我们用的都不多,主要是用前面两个
下面是使用代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);// 创建一个可缓存的线程池(线程数量动态调整)ExecutorService cachedThreadPool = Executors.newCachedThreadPool();}
}
这代码我们有个疑点,我们并没有new一个对象,那我们是怎么创建出来对象的呢?
这个问题涉及到工厂模式这种设计模式:
工厂模式是一种常用的设计模式,用于封装对象的创建逻辑。它通过使用方法来创建对象(new在方法内部),而不是直接使用 new 关键字实例化对象。这样可以将对象的创建逻辑与使用逻辑解耦,提高代码的可维护性和可扩展性。
这里就是用方法创建出对象,所以涉及到了工厂模式
ThreadPoolExecutor类(直接new)
对于刚才讲的 Executors 本质上是 ThreadPoolExecutor 类的封装.而对于ThreadPoolExecutor类本身我们提供了更多的可选参数, 可以进一步细化线程池行为的设定.
如下图是 ThreadPoolExecutor类的构造方法:
核心线程数(corePoolSize):线程池中始终保持的线程数量。这是不会被销毁的。
最大线程数(maximumPoolSize):线程池中允许的最大线程数量。这种一般涉及到刚才的动态线程池,如果任务多了则创建一些线程,多了的话过了一段时间则会销毁,但核心线程数不变。
空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。
任务队列(workQueue):其为阻塞队列,用于存储等待执行的任务。要记住,当我们创建线程池时,系统也会同时自动创建一个阻塞队列去存储等待执行的任务,这样效率就更高。
线程工厂(threadFactory):线程工厂是一个用于创建线程的工具类或接口,它允许用户自定义线程的创建逻辑,开发者可以控制线程的名称、优先级、异常处理等属性,从而更好地管理线程资源。
拒绝策略(handler):当线程池已满且阻塞队列也已满时,新任务的处理策略。
下面重点讲述一下拒绝策略:
- AbortPolicy:直接抛出 RejectedExecutionException 异常。(当导员给我一个任务“统计班级成员中团员个数‘’,但是我现在已经课很多了,我一下子就哭了出来)这个就相当于直接抛异常
- CallerRunsPolicy:由提交任务的线程直接执行任务。(我直接给导员说,我没空,导员最后只能自己做了)
- DiscardPolicy:直接丢弃任务,不抛出异常。(导员一听我没空,就直接说,好!那我也不统计了,随便来了)
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务。(我听到导员的任务的时候,我选择放弃我最早出现的一节课去帮导员完成任务)
下面是其创建代码
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, // 核心线程数4, // 最大线程数60, // 空闲线程存活时间TimeUnit.SECONDS, // 时间单位new ArrayBlockingQueue<>(10), // 任务队列,容量为 10Executors.defaultThreadFactory(), // 线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略);
总结一下:
工厂模式创建线程:适合简单的线程池创建场景,代码简单,但灵活性有限。
构造方法创建线程:适合需要灵活配置线程池属性的场景,通过自定义线程池,可以更好地管理线程资源,提高代码的可维护性和可扩展性。
submit
通过线程池.submit(继承runable的类的对象) 可以提交一个任务到线程池中执行.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}
});
实现一个线程池
这里就直接上代码了,不多说,重点还是使用线程池,不是实现线程池。
/*** 自定义线程池执行器类* 该类通过实现一个具有固定大小的线程池和一个阻塞队列来管理线程,用于异步执行任务*/
class MyThreadPoolExecutor {// 创建阻塞队列,用于存放待执行的任务// 队列大小设为1000,用于控制并发任务的数量,避免过多任务导致资源耗尽BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(1000);/*** 构造函数,初始化线程池* 创建一个线程,该线程循环从阻塞队列中取任务并执行* 这个线程是线程池中的工作线程,负责执行提交的任务*/public MyThreadPoolExecutor(int n) {for (int i = 1; i <= n; i++) {Thread t = new Thread(() -> {// 无限循环,确保线程池可以持续处理任务,直到程序中断或阻塞队列被清空while (true) {try {// 从阻塞队列中取出一个任务,如果队列为空,则线程被阻塞,直到有任务放入队列Runnable task = blockingQueue.take();// 执行取出的任务task.run();} catch (InterruptedException e) {// 如果线程在等待状态时被中断,抛出运行时异常// 这通常会导致程序异常终止throw new RuntimeException(e);}}});// 启动线程池中的工作线程t.start();}}/*** 提交一个任务到线程池* @param task 需要被执行的任务* 任务被放入阻塞队列中,随后由线程池中的工作线程执行*/public void submit(Runnable task){// 将任务放入阻塞队列,如果队列已满,则操作会阻塞,直到有空间可用blockingQueue.offer(task);}
}
class DemoTest1{public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor ex=new MyThreadPoolExecutor(4);for(int i=0;i<100;i++) {int id = i;ex.submit(()->{System.out.println(Thread.currentThread().getName()+" 任务:"+id);});}}
}
多线程基础知识点到这里就告一段路了,接下来我们将学习多线程(进阶)这部分是主要讲面试中经典题,频繁的题