作为 Java 程序员,无论是技术面试、项目研发或者是学习框架源码,不彻底掌握 Java 多线程的知识,做不到心中有数,干啥都没底气,尤其是技术深究时往往略显发憷。
1
回顾:创建线程的几种方式?
在 Java 的世界里,大家最熟悉的线程的创建方式,莫过于 Java 提供的 Thread 类和 Runnable 接口。
核心知识点(一):继承 Thread 类 VS 实现 Runnable 接口的区别?
从 JDK1.5 开始,Java 提供了 Callable 接口,提供另一种创建线程的方式。
核心知识点(二):实现 Callable 接口创建线程,有啥独特?
使用实现 Callable 接口的方式创建的线程,相对于继承 Thread 类和实现 Runnable 接口来创建线程的方式而言,可以获取到线程执行的返回值、以及是否执行完成等信息。
2
思考:无限制的创建线程,会带来什么问题?
在项目开发中,为了提高系统的吞吐量和性能,很多同学都会随手写出如下最简单的线程创建代码。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是一个孤独的线程");
// do something
}
}).start();
有的同学,会采用 Lambda 表达式,写出如下稍显高 B 格的代码。
new Thread(() -> System.out.println("我是一个孤独的线程")).start();
在简单的应用中,上面的代码没有太大的问题。但是如果在业务量较大,可能要开启很多线程来处理,而当线程数量过大时,反而会耗尽 CPU 和内存资源,如果处理不当,可能会导致 Out of Memory 异常。
写段代码,简单示意一下内存溢出。
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 10000; i++) {
notifyMsgList.add("发工资啦" + i);
}
// 通知用户
for (String msg : notifyMsgList) {
// 多线程发送
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通知:" + msg);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
程序跑起来,大概率会出现内存溢出的异常。
java.lang.OutOfMemoryError: unable to create new native thread
贴一效果图,真的不诳你。
这么看,线(劲)程(酒)虽好,不能贪多(杯)呀。在生产环境中,线程的数量必须要进行控制,不然盲目的创建大量线程会对系统性能造成伤害,甚至会导致内存溢出,拖垮应用。
那该怎么办?这就很有必要引入线程池啦。
3
Executor 框架入门:别造轮子啦,JDK 都给你提供啦!
线程池的基本功能就是进行线程的复用,当系统接受一个提交的任务,需要一个线程时,并不立即去创建线程,而是先去线程池查找是否有空余的线程,若有,则直接使用线程池中的线程进行工作,若没有,再去创建新的线程。待任务完成后,也销毁线程,而是将线程放入线程池的空闲对列,等待下次使用。
在线程频繁调度的场景中,JDK1.5 以前,攻城狮必须手动打造线程池,来节约系统开销;而从 JDK1.5 开始,Java 提供了一个 Excutors 工厂类来生产线程池,可以帮助攻城狮有效的进行线程控制。
核心知识点(一):Excutors 工厂类的主要方法有哪些?
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
上面列举的方法中,分别返回了不同工作特性的线程池,下面简单介绍一下每个方法。
1. newFixedThreadPool(int nThreads)
用途:创建一个可重用的、具有固定线程数的线程池。
备注:该线程池中的线程数量始终不变,当有一个新的任务提交时,线程中若有空闲线程,则立即执行,若没有则空闲线程,新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
2. newSingleThreadExecutor()
用途:创建一个只有一个线程的线程池,相当于 newFixedThreadPool(int nThreads) 方法调用时传入的参数为 1。
备注:该线程池中的线程数量为 1。
3. newCachedThreadPool()
用途:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程会被缓存在线程池中。
备注:该线程池中的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程,若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
4. newSingleThreadScheduledExecutor()
用途:创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。
备注:线程池大小为 1,并且可以在固定的延时之后执行或者周期性执行某个任务。
5. newScheduledThreadPool(int corePoolSize)
用途:创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。
备注:corePoolSize 指池中所保存的线程数,即使线程是空闲的,也被保存在线程池内。
上面列举了 Excutors 类中的一些方法,仔细去看,会发现前三个返回一个 ExcutorService 对象,而后两个方法返回 ScheduledExecutorService 对象。
核心知识点(二):ExcutorService 与 ScheduledExecutorService 啥区别?
走进源码,从方法定义上着重分析一下区别。
首先截取 ExcutorService 的部分方法定义,重点关注 submit 重载的方法。
上面截图释义:
了解完 ExcutorService,那么来看看 ScheduledExecutorService 的方法定义。
上面截图释义:
了解完方法定义,区别就很简单了:
4
Executor 框架使用:说的再多,都不如写一行代码!
还是以开篇发工资的场景为例,若借助线程池来实现,假设允许开启 10 个线程来进行发工资(理解成 10 个人同时干活就行),代码改造如下。
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 10000; i++) {
notifyMsgList.add("发工资啦" + i);
}
// 1. 调用 Excutors 类的静态工厂方法创建一个 ExcutorService 对象(线程池);
ExecutorService executorService = Executors.newFixedThreadPool(100);
// 通知用户
for (String msg : notifyMsgList) {
// 2. 创建 Runnable 实现类或者 Callable 实现类的实例,作为线程执行任务。
// 3. 调用 ExcutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例。
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("通知:" + msg);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//4. 调用 ExcutorService 对象的 shutdown 方法来关闭线程池。
executorService.shutdown();
程序跑起来,很清晰的能看出有 10 个线程同时在处理任务。
好了,到这简单总结一下,使用线程池来编程时的步骤(仔细看代码注释就行啦)。
既然提到了关闭线程池的 shutdown 方法,那再抛一知识点:shutdown 与 shutdownNow 的区别是啥?
写段程序很容易发现结论,还是借助上面发工资通知的代码,把通知的消息改为 11,把线程数改为 10。
首先调用 shutdown 方法关闭线程池,代码如下。
// 待通知的消息
List<String> notifyMsgList = new ArrayList<String>(10000);
for (int i = 0; i < 11; i++) {
notifyMsgList.add("发工资啦" + i);
}
// 1. 调用 Excutors 类的静态工厂方法创建一个 ExcutorService 对象(线程池);
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 通知用户
for (String msg : notifyMsgList) {
// 2. 创建 Runnable 实现类或者 Callable 实现类的实例,作为线程执行任务。
// 3. 调用 ExcutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例。
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ "-->通知:" + msg);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//4. 调用 ExcutorService 对象的 shutdown 方法来关闭线程池。
executorService.shutdown();
输出截图(执行完了,才优雅的终止):
接着改造代码,调用 shutdownNow 方法来关闭线程池,代码如下。
//4. 调用 ExcutorService 对象的 shutdownNow 方法来关闭线程池。
List<Runnable> tasks = executorService.shutdownNow();
System.out.println(tasks);
输出截图:
结论:
5
寄语写最后
本次,本次主要回顾了创建线程的方式,并对 JDK 对任务执行框架 Excutor 进行初步讲解,后续会带你一起走进这个框架背后隐藏的 ThreadPoolExecutor 类,一起去探寻线程池背后的奥秘。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有