Java多线程那些事(二)

Java多线程那些事(二)

大家好,我是栗子为。

“最近小为忙着毕业,刚结束论文答辩,想起来上次关于Java多线程的事情还没说完呢,这不,立马准备带着大家拿下这一面试高频问题。

上次介绍了创建线程的方式、线程的生命周期以及Thread类常用的方法,具体可看小为的上篇文章《Java多线程那些事(一)》

咱们话不多说,一起来看看今天的知识点…



01


线程池



为什么要使用线程池?

  1. 可以降低资源的消耗。减少创建和销毁线程造成的消耗。
  2. 提高响应速度。当有任务需要执行时,可直接从线程池里拿到线程,不需要重新创建。
  3. 统一管理。能对资源进行统一分配、调优和监控。

如何创建线程池?

线程池的创建方法分为利用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:拒绝策略。当线程池达到最大线程数时需要执行的饱和策略
  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。线程池的默认拒绝策略,如果是比较关键的业务,推荐使用此拒绝策略,当系统不能承载更大的并发量的时候,能及时通过异常发现
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。建议一些无关紧要的业务采用此策略,例如统计博客网站的阅读量
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
  4. ThreadPoolExecutor.CallerRunsPolicy:由提交任务的线程处理该任务

补充常用的workQueue,以下均为线程安全

参数 描述
ArrayBlockingQueue 数组有界的阻塞队列
LinkedBlockingQueue 链表有界的阻塞队列
SynchronousQueue 不存储元素的阻塞队列,直接提交给线程
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
DelayQueue 使用优先级队列实现的无界阻塞队列,过了延迟时长才能提取元素
LinkedTransferQueue 链表无界的阻塞队列。与SynchronousQueue相似,含有非阻塞方法
LinkedBlockingDeque 链表双向阻塞队列

线程池的工作流程

Java多线程那些事(二)
线程池的工作流程

简单点说:

  1. 当线程数小于核心线程数时,创建线程
  2. 当线程数大于或等于核心线程数时,若任务队列未满,将任务放入队列中
  3. 当线程数大于或等于核心线程数时,若任务队列已满,若线程数小于最大线程数,则创建线程,否则抛出异常,按饱和策略进行处理



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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!