大家好,我是栗子为。
“最近小为忙着毕业,刚结束论文答辩,想起来上次关于Java多线程的事情还没说完呢,这不,立马准备带着大家拿下这一面试高频问题。
上次介绍了创建线程的方式、线程的生命周期以及Thread类常用的方法,具体可看小为的上篇文章《Java多线程那些事(一)》”
咱们话不多说,一起来看看今天的知识点…
01
—
线程池
为什么要使用线程池?
-
可以降低资源的消耗。减少创建和销毁线程造成的消耗。 -
提高响应速度。当有任务需要执行时,可直接从线程池里拿到线程,不需要重新创建。 -
统一管理。能对资源进行统一分配、调优和监控。
如何创建线程池?
线程池的创建方法分为利用Executors工厂类和利用ThreadPoolExecutor类两种。
Executors类常用的四种线程池创建方法
-
newCachedThreadPool
创建一个
可缓存的无界线程池
,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。当线程池中的线程空闲时间超过60s
,则会自动回收该线程
,当任务超过线程池的线程数则创建新的线程,线程池的大小上限为Integer.MAX_VALUE,可看作无限大。该线程池中没有核心线程
,非核心线程的数量为Integer.max_value
,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况
。
举个🌰
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
// 结果如下
2022-05-30T15:39:55.080 pool-1-thread-4 3
2022-05-30T15:39:55.080 pool-1-thread-2 1
2022-05-30T15:39:55.080 pool-1-thread-6 5
2022-05-30T15:39:55.080 pool-1-thread-7 6
2022-05-30T15:39:55.080 pool-1-thread-8 7
2022-05-30T15:39:55.080 pool-1-thread-3 2
2022-05-30T15:39:55.080 pool-1-thread-1 0
2022-05-30T15:39:55.080 pool-1-thread-5 4
2022-05-30T15:39:55.080 pool-1-thread-9 8
2022-05-30T15:39:55.080 pool-1-thread-10 9
// 任务数超过了线程数,所以每个任务都要创建新的线程,线程名不相同
-
newFixedThreadPool
创建一个
指定大小的线程池
,可控制线程的最大并发数,超出的线程会在LinkedBlockingQueue阻塞队列中等待。定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
举个🌰
public static void main(String[] args) {
// 设定线程池大小为3
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
// 结果如下
2022-05-30T15:47:56.202 pool-1-thread-1 0
2022-05-30T15:47:56.202 pool-1-thread-2 1
2022-05-30T15:47:56.202 pool-1-thread-3 2
2022-05-30T15:47:58.205 pool-1-thread-3 3
2022-05-30T15:47:58.206 pool-1-thread-1 4
2022-05-30T15:47:58.206 pool-1-thread-2 5
2022-05-30T15:48:00.209 pool-1-thread-1 8
2022-05-30T15:48:00.209 pool-1-thread-3 6
2022-05-30T15:48:00.209 pool-1-thread-2 7
2022-05-30T15:48:02.214 pool-1-thread-2 9
// 任务数量为10,线程池大小为3,只会创建3个线程,由于线程数量不足,会进入队列等待线程空闲
-
newScheduledThreadPool
创建一个
定长的线程池
,可以指定线程池核心线程数,支持定时及周期性任务的执行
。周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务
举个🌰
public static void main(String[] args) {
// 需要给定线程池长度
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println(LocalDateTime.now() + "提交任务");
for (int i = 0; i < 10; i++) {
final int index = i;
// 调用schedule方法,其参数为schedule(Runnable command, long delay, TimeUnit unit)
executorService.schedule(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 3, TimeUnit.SECONDS);
}
}
// 结果如下
2022-05-30T16:02:59.320提交任务
2022-05-30T16:03:02.335 pool-1-thread-1 0
2022-05-30T16:03:02.335 pool-1-thread-3 2
2022-05-30T16:03:02.335 pool-1-thread-2 1
2022-05-30T16:03:04.340 pool-1-thread-2 3
2022-05-30T16:03:04.340 pool-1-thread-1 4
2022-05-30T16:03:04.340 pool-1-thread-3 5
2022-05-30T16:03:06.345 pool-1-thread-1 7
2022-05-30T16:03:06.345 pool-1-thread-3 8
2022-05-30T16:03:06.345 pool-1-thread-2 6
2022-05-30T16:03:08.350 pool-1-thread-1 9
// 根据延迟参数delay,所以提交后3秒才开始执行任务,因为这里设置核心线程数为3个,而线程不足会进入队列等待线程空闲,所以日志间隔2秒输出
-
newSingleThreadExecutor
创建一个
单线程化
的线程池,它只有一个线程,用仅有的一个线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行
,所有的任务都保存在队列LinkedBlockingQueue中,等待唯一的单线程来执行任务。只有一条线程来执行任务,适用于有顺序的任务的应用场景
举个🌰
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
// 调用schedule方法,其参数为schedule(Runnable command, long delay, TimeUnit unit)
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
// 结果如下
2022-05-30T16:09:39.066 pool-1-thread-1 0
2022-05-30T16:09:41.072 pool-1-thread-1 1
2022-05-30T16:09:43.076 pool-1-thread-1 2
2022-05-30T16:09:45.081 pool-1-thread-1 3
2022-05-30T16:09:47.085 pool-1-thread-1 4
2022-05-30T16:09:49.089 pool-1-thread-1 5
2022-05-30T16:09:51.092 pool-1-thread-1 6
2022-05-30T16:09:53.095 pool-1-thread-1 7
2022-05-30T16:09:55.097 pool-1-thread-1 8
2022-05-30T16:09:57.101 pool-1-thread-1 9
// 可以看到只有一个线程,任务按顺序执行
-
方法对比
工厂方法 | corePoolSize | maximumPoolSize | keepAliveTime | workQueue |
---|---|---|---|---|
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60s | SyschoronousQueue |
newFixedThreadPool | n | n | 0 | LinkedBlockingQueue |
newSingleThreadExecutor | 1 | 1 | 0 | LinkedBlockingQueue |
newScheduledThreadPool | n | Integer.MAX_VALUE | 0 | DelayedWorkQueue |
利用ThreadPoolExecutor类
ThreadPoolExecutor
类的构造方法如下
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){}
可以看到,构造方法需要以下的一些参数:(加粗的即为必需参数,其余为非必须)
-
corePoolSize:核心线程数。默认核心线程会一直存活,如果将 allowCoreThreadTimeout
设置为true
,核心线程一旦超时也会被回收 -
maximumPoolSize:线程池能容纳的最大线程数。当活跃的线程数达到该值后,新任务将会被阻塞 -
keepAliveTime:线程闲置超时时长。如果超过该时长, 非核心线程
就会被回收。如果将allowCoreThreadTimeout
设置为true
,核心线程一旦超时也会被回收 -
unit:指keepAliveTime参数的时间单位。常用的有 TimeUnit.MILLISECONDS(毫秒)
、TimeUnit.SECONDS(秒)
、TimeUnit.MINUTES(分钟)
-
workQueue:任务队列,用来存储等待执行的任务,通过线程池的 execute()
方法提交的Runnable对象将存储在该参数中。采用阻塞队列
实现 -
threadFactory:线程工厂。指定为线程池创建新线程的方式 -
handler:拒绝策略。当线程池达到最大线程数时需要执行的饱和策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。线程池的默认拒绝策略,如果是比较关键的业务,推荐使用此拒绝策略,当系统不能承载更大的并发量的时候,能及时通过异常发现 ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。建议一些无关紧要的业务采用此策略,例如统计博客网站的阅读量 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 ThreadPoolExecutor.CallerRunsPolicy:由提交任务的线程处理该任务
补充常用的workQueue,以下均为线程安全
参数 | 描述 |
---|---|
ArrayBlockingQueue | 数组有界的阻塞队列 |
LinkedBlockingQueue | 链表有界的阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,直接提交给线程 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的无界阻塞队列,过了延迟时长才能提取元素 |
LinkedTransferQueue | 链表无界的阻塞队列。与SynchronousQueue相似,含有非阻塞方法 |
LinkedBlockingDeque | 链表双向阻塞队列 |
线程池的工作流程

简单点说:
-
当线程数小于核心线程数时,创建线程 -
当线程数大于或等于核心线程数时,若任务队列未满,将任务放入队列中 -
当线程数大于或等于核心线程数时,若任务队列已满,若线程数小于最大线程数,则创建线程,否则抛出异常,按饱和策略进行处理
02
—
多线程常考面试题
线程的同步
在面试中,经常会被问到一种题,“如果只放出了5张票,多线程抢票会造成什么问题,如何避免这些问题?”
这就要考虑到多线程的同步问题,在Java中,常用的同步方式就是加锁,例如Synchronized同步方法或同步代码块都能保证数据的一致性,关于Java锁的内容很多,就不在这篇文章做过多介绍了,我们来看看如果遇到这样的场景,如何用代码来解决
// 方式一:同步块
class MyThread implements Runnable {
private int ticket = 10; // 模拟10张票
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) { // 对当前对象进行同步
if (ticket > 0) { // 当还有票时
try {
Thread.sleep(300); // 加入延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("还剩" + ticket-- + "票");
}
}
}
}
}
// 方式二:同步方法
class MyThread implements Runnable{
private int ticket = 10 ; // 模拟10张票
public void run(){
for(int i=0;i<100;i++){
this.sale() ; // 调用同步方法
}
}
public synchronized void sale(){ // 声明同步方法
if(ticket>0){ // 还有票
try{
Thread.sleep(300) ; // 加入延迟
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println("还剩" + ticket-- + "票");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread(); // 定义线程对象
Thread thread1 = new Thread(mt);
Thread thread2 = new Thread(mt);
Thread thread3 = new Thread(mt);
thread1.start();
thread2.start();
thread3.start();
}
}
// 结果如下
还剩10票
还剩9票
还剩8票
还剩7票
还剩6票
还剩5票
还剩4票
还剩3票
还剩2票
还剩1票
死锁问题
因为小为之前面试就遇到过面试官要我现场写一个死锁的场景,所以在这里给大家分享一下
简单说一下死锁问题就是两个线程等待对方先完成,从而造成了程序的停滞
class ChestNut1 { // 定义栗子为
public void say() {
System.out.println("栗子为对花栗鼠小K说:“你点赞我的文章,我就评论你的文章。”");
}
public void get() {
System.out.println("栗子为的文章被点赞...");
}
};
class ChestNut2 { // 定义花栗鼠小K
public void say() {
System.out.println("花栗鼠小K对栗子为说:“你评论我的文章,我就点赞你的文章”");
}
public void get() {
System.out.println("花栗鼠小K的文章收到了评论...");
}
};
public class ThreadDeadLock implements Runnable {
private static ChestNut1 chestNut1 = new ChestNut1(); // 实例化static型对象
private static ChestNut2 chestNut2 = new ChestNut2(); // 实例化static型对象
private boolean flag = false; // 声明标志位,判断哪个先说话
@Override
public void run() { // 覆写run()方法
if (flag) {
synchronized (chestNut1) { // 同步栗子为
chestNut1.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chestNut2) {
chestNut1.get();
}
}
} else {
synchronized (chestNut2) {
chestNut2.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chestNut1) {
chestNut2.get();
}
}
}
}
public static void main(String[] args) {
ThreadDeadLock t1 = new ThreadDeadLock(); // 控制栗子为
ThreadDeadLock t2 = new ThreadDeadLock(); // 控制花栗鼠小K
t1.flag = false;
t2.flag = true;
Thread thA = new Thread(t1);
Thread thB = new Thread(t2);
thA.start();
thB.start();
}
}
// 结果如下
花栗鼠小K对栗子为说:“你评论我的文章,我就点赞你的文章”
栗子为对花栗鼠小K说:“你点赞我的文章,我就评论你的文章。”
由于相互都持有锁而不释放,程序不会往下进行,只有一边释放锁,程序才能往下进行
03
—
总结
以上就是Java多线程中很重要的内容啦,花了两篇文章的时间,总结一下Java多线程会问到以下这些问题
(1)如何创建线程
(2)线程的生命周期
(3)常用线程池、线程池的使用、线程池的工作流程
(4)能否写个线程同步的场景、能否写个死锁的场景
希望大家看完后能很自信的和面试官battle…..
好了,今天的文章就分享到这,我下次再来叭叭
关注六只栗子,面试不迷路
作者 栗子为
编辑 一口栗子
原文始发于微信公众号(六只栗子):Java多线程那些事(二)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88693.html