文章目录

  • 一、线程池的重要性
  • 1、为什么使用线程池
  • 2、不使用线程池怎么处理
  • 3、使用线程池的好处
  • 4. 线程池适合的场景
  • 二、线程池的创建和停止
  • 1、线程池构造函数的参数
  • 2、线程工作流程
  • 三、JDK提供给我们的线程池
  • 1、newFixThreadPool
  • 2、newSingleThreadExecutor
  • 3、newCacheThreadPool
  • 4、newScheduledThreadPool
  • 5、以上几种线程池对比
  • 1).为什么把FixThreadPool和SingleThreadPool阻塞队列设置为LinkedBlokingQueue?
  • 2).为什么CacheThreadPool使用的是SynchronousQueue?
  • 四、自定义线程池
  • 1、线程池数量设置多少合适?
  • 五、停止线程池的正确方法
  • 1、 shutDown
  • 2、ShutDownNow
  • 六、拒绝策略
  • 1、AbortPolicy
  • 2、DiscardPolicy
  • 3、DiscardOldestPolicy
  • 4、CallerRunsPolicy
  • 七、线程池状态
  • 八、线程池使用注意点


一、线程池的重要性

1、为什么使用线程池

  1. 可以复用线程
  2. 控制资源的总量
  3. 反复创建销毁线程开销大
  4. 过多线程占用太多的资源

2、不使用线程池怎么处理

  1. 每个任务都需要新开一个线程处理
  2. for循环处理线程
public class EveryTaskOneThread {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Task());
            thread.start();
        }
    }
}
 class Task implements Runnable{

     @Override
     public void run() {
         System.out.println(Thread.currentThread().getName()+":执行了任务");
     }
 }

java基础线程面试题 java线程 面试题_面试

  1. 当任务有一万个

这样任务开销太大,我们希望有固定的线程,处理这些任务,避免了线程返回创建销毁所带来的开销问题。

3、使用线程池的好处

  1. 加快响应速度
  2. 合理利用cpu和内存
  3. 统一管理

4. 线程池适合的场景

  1. 服务器接收大量请求时。
  2. 需要创建5个以上线程时。

二、线程池的创建和停止

1、线程池构造函数的参数

java基础线程面试题 java线程 面试题_java_02

2、线程工作流程

  1. 默认情况下,创建完线程池后并不会立即创建线程, 而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
  2. 当线程数小于核心线程数时,每提交一个任务就创建一个线程来执行,即使当前有线程处于空闲状态,直到当前线程数达到核心线程数。
  3. 当前线程数达到核心线程数时,如果这个时候还提交任务,这些任务会被放到队列里,等到线程处理完了手头的任务后,会来队列中取任务处理。
  4. 当前线程数达到核心线程数并且队列也满了,如果这个时候还提交任务,则会继续创建线程来处理,直到线程数达到最大线程数。
  5. 当前线程数达到最大线程数并且队列也满了,如果这个时候还提交任务,则会触发饱和策略。
  6. 如果某个线程的控线时间超过了keepAliveTime,那么将被标记为可回收的,并且当前线程池的当前大小超过了核心线程数时,这个线程将被终止。

图解:

java基础线程面试题 java线程 面试题_线程池_03

三、JDK提供给我们的线程池

1、newFixThreadPool

java基础线程面试题 java线程 面试题_经验分享_04

public class newFixThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task1());
        }
    }
}

class Task1 implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

执行结果

pool-1-thread-2
pool-1-thread-4
pool-1-thread-5
pool-1-thread-3
pool-1-thread-1
pool-1-thread-2
pool-1-thread-5
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-1
pool-1-thread-2
pool-1-thread-4
pool-1-thread-3
pool-1-thread-5

从上面我们可以看出,始终是线程1到5。

从源码分析,newFixThreadPool的核心线程数和最大线程数相同,使用的是LinkedBlockingQueue无界队列,如果请求越来越多,且无法即使处理完,容易OOM内存溢出。

下面是内存溢出的情况:

public class newFixThreadPoolOOM {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executorService.execute(new Task2());

        }
    }
}

class Task2 implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(5000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

2、newSingleThreadExecutor

代码演示:

public class SingleThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100; i++) {
            singleThreadExecutor.execute(new Task1());
        }
    }
}

执行结果:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

特点
只有一个线程,和newFixThreadPool原理一样,只不过把线程数据直接设置成了1,大量请求堆积的时候,也会占用大量的内存。

3、newCacheThreadPool

可缓存、能够回收

原理:

java基础线程面试题 java线程 面试题_经验分享_05

代码

public class newCacheThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task1());
        }
    }
}

运行结果

pool-1-thread-100
pool-1-thread-148
pool-1-thread-123
pool-1-thread-77
pool-1-thread-93
pool-1-thread-62
pool-1-thread-129
pool-1-thread-121
pool-1-thread-54
pool-1-thread-32
pool-1-thread-119
pool-1-thread-44
pool-1-thread-21
pool-1-thread-135
pool-1-thread-55
pool-1-thread-163
pool-1-thread-156
pool-1-thread-164
pool-1-thread-81
pool-1-thread-171
pool-1-thread-172
pool-1-thread-179
pool-1-thread-82
pool-1-thread-180
pool-1-thread-76
pool-1-thread-75
pool-1-thread-89
pool-1-thread-68

pool-1-thread-913
pool-1-thread-946
pool-1-thread-952
pool-1-thread-905

弊端是最大线程数设置为了Integer的最大值,会导致无限创建线程,甚至导致OOM

4、newScheduledThreadPool

支持定期及周期性执行的线程池

5、以上几种线程池对比

1).为什么把FixThreadPool和SingleThreadPool阻塞队列设置为LinkedBlokingQueue?

因为任务比较多,没办法存储,所以需要无界队列

2).为什么CacheThreadPool使用的是SynchronousQueue?

因为最大线程数是integer的最大值,不需要队列进行缓存了。

java基础线程面试题 java线程 面试题_java基础线程面试题_06

四、自定义线程池

我们应该根据自己的业务场景,设置线程池的自定义参数,比如线程大小,线程名字等。

1、线程池数量设置多少合适?

要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是I/O密集型。

对于计算密集型,设置 线程数 = CPU数 + 1,通常能实现最优的利用率。

对于I/O密集型,网上常见的说法是设置线程数 = CPU数 * 2,这个做法是可以的,但个人觉得不是最优的。

在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:

线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务工作时间 + 1)

例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。

当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。

以上内容转自囧辉 原文链接:

五、停止线程池的正确方法

1、 shutDown

shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。

代码

public class ShutDownTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());
        executorService.shutdown();
        System.out.println(executorService.isShutdown());
        executorService.execute(new ShutDownTask());
    }
}

class ShutDownTask implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果

pool-1-thread-1
pool-1-thread-9
false
pool-1-thread-10
pool-1-thread-2
pool-1-thread-5
pool-1-thread-6
pool-1-thread-8
true
Exception in thread “main” java.util.concurrent.RejectedExecutionException: Task com.wy.threadpool.ShutDownTask@67b64c45 rejected from java.util.concurrent.ThreadPoolExecutor@4411d970[Shutting down, pool size = 10, active threads = 10, queued tasks = 960, completed tasks = 30]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
at com.wy.threadpool.ShutDownTest.main(ShutDownTest.java:16)
pool-1-thread-6
pool-1-thread-8
pool-1-thread-3

2、ShutDownNow

“粗暴”的关闭线程池,也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

六、拒绝策略

  1. 当Executor关闭,提交新任务会被拒绝
  2. 当使用有界队列到达最大线程数时已经饱和时被拒绝

1、AbortPolicy

AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

2、DiscardPolicy

DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

3、DiscardOldestPolicy

DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

4、CallerRunsPolicy

CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。简而言之,让主线程去跑。

个人感觉CallerRunsPolicy最好,给线程池一段缓存的时间,也不会抛弃任务、也不抛出异常。

七、线程池状态

相同线程执行不同任务
RUNNING:接受新任务并处理排队的任务。

SHUTDOWN:不接受新任务,但处理排队的任务。

STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。

TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。

TERMINATED:terminated() 已完成。

java基础线程面试题 java线程 面试题_线程池_07

八、线程池使用注意点

  1. 避免任务堆积
  2. 避免线程过度增加
  3. 排查线程泄露,任务执行完毕,但没有被回收