线程池面试汇总
Posted IT-老牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池面试汇总相关的知识,希望对你有一定的参考价值。
文章目录
1.进程Process与线程Thread
进程:就是正在执行的程序,线程是程序执行的一条路径,一个进程之中可以包含多个线程。通俗的讲,我们可以把打开微信开做一个进程,在微信里和一个好友进行视频聊天就是开启了一个线程。
线程:一个进程里可以有多条线程,至少有一条线程,我认为可以理解为当你开启进程时默认开启一条线程,一条线程一定会在一条进程里,就好像你不打开微信就没有办法和微信里的好友进行聊天。
2.创建多线程4种方式
方式一:实现类继承Thread类
步骤:
①实现类去继承Thread类;
②实现类重写Thread类中的run()方法;
③测试类创建Thread子类实例;
④开启线程,调用Thread类中的start()方法。
方式二:实现Runnable接口
步骤:
①实现Runnable接口;
②重写run方法;
③实例化实现类;
④将实现类以参数传递给Thread对象;
⑤开启线程。
方式三: 实现Callable
接口,实例化FutureTask
类(jdk.1.5出现)
步骤:
①实现Runnable接口;
②重写run方法;
③实例化实现类;
④实现类以参数的形式传递到FutureTask对象;
⑤FutureTask对象以参数的形式传递到Thread对象中;
⑥此步骤可有可无,FutureTask对象调用get()方法,获取到call方法的返回值;
重写的Call()方法特性:
1.call()方法有返回值;②call()方法会抛异常;③call()方法支持泛型
实现Callable
接口 Java测试代码:
package TreadTest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
线程创建方式三:
实现Callable接口
*/
class thread4 implements Callable
public Object call() throws Exception
int sum = 0;
for (int i = 0; i < 5; i++)
sum += i;
return sum;
public class ThreadTest3
public static void main(String[] args)
thread4 t4 = new thread4();
FutureTask<Integer> task = new FutureTask<Integer>(t4);
new Thread(task).start();
try
Integer integer = task.get();//调用get()方法要抛异常,返回call方法的返回值
System.out.println("sum=" + integer);
catch (InterruptedException e)
e.printStackTrace();
catch (ExecutionException e)
e.printStackTrace();
执行结果:(会返回call()方法的返回值)
sum=10
Callable其实就是一接口,跟Runnable差不多,只不多有返回值,它需要有一容器来包装它就是FutureTask类。
方式四: 线程池
Executor
框架介绍:在jdk1.5加入Executor框架,Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
背景:
经常创建和销毁,使用量特别大的资源时,比如并发情况下的线程,对性能影响很大。
思路:
提前创建多个线程,放入线程池,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用
好处:
1、提高响应速度(减少了创建新线程的时间)
2、降低资源消耗(重复利用线程池中线程,不需要每次创建)
3、便于线程管理
使用:
JKD5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExceptor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池
Executors:工具类,线程池的工厂类,用户创建并返回不同类型的线程池
package com.bruce.demo9;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//测试线程池
public class TestPool
public static void main(String[] args)
//创建服务,创建线程池
//newFixedThreadPool 参数:线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行
executorService.execute(new MyThread(1));
executorService.execute(new MyThread(2));
executorService.execute(new MyThread(3));
executorService.execute(new MyThread(4));
//关闭连接
executorService.shutdown();
class MyThread implements Runnable
int id;
public MyThread(int id)
this.id = id;
@Override
public void run()
System.out.println(Thread.currentThread().getName()+"执行任务"+id);
3.继承Thread类和实现Runnable接口比较
相同点
①都可以创建多个线程;
②都需要重写Thread类中的run()方法;
③都需要通过start()方法进行开启;
④run方法就是void类型的,没有返回值;
差别
①关键字不一样,继承是extends Thread
,实现implements Runnable
;
②通过实现Runnable
接口创建的线程要比继承Thread
这种方式灵活得多。其实查看源码,我们可以知道,Thread类其实也是继承于Runnable接口。由于Java是单继承,可多实现的特性,当实现类要继承于其他类时,就不能再继承Thread类了,所有Java就因此又多了两种创建方式一是实现runnable接口(方法二),二是实现Callable接口(方式三)。
③由于多个线程共享一个Runnable
对象,所以Runnable
适合多个相同的线程去处理共享资源。这就涉及到线程同步了。
4.使用线程池比手动创建线程好在哪里
1、减少线程生命周期带来的开销。如:线程是提前创建好的,可以直接使用,避免创建线程的消耗。
2、合理的利用内存和CPU
。如:避免线程创建较多造成的内存溢出,避免线程创建较少造成CPU
的浪费。
3、可以统一管理资源。如:统一管理任务队列,可以统一开始或结束任务。
/**
* 例子: 用固定线程数的线程池执行10000个任务
*/
public class ThreadPoolDemo
public static void main(String[] args)
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10000; i++)
service.execute(new Task());
System.out.println(Thread.currentThread().getName());
static class Task implements Runnable
public void run()
System.out.println("Thread Name: " + Thread.currentThread().getName());
5.线程池的各参数的含义
corePoolSize
:核心线程数(“长工”),常驻线程的数量。随着任务增多,线程池从0开始增加。
maxPoolSize
:最大线程数,创建线程的最大容量。是核心线程数与非核心线程数之和。
keepAliveTime+时间单位
:空闲线程存活时间。当非核心线程(“临时工”)空闲时,过了存活时间该线程就会被- 回收 。
ThreadFactory
:创建线程的工厂。
workQueue
:存放任务的队列。任务队列满了,会创建非核心线程,直至达到最大线程数。
Handler
:任务拒绝策略。当线程数达到最大,并且队列被塞满时,会拒绝任务。
6.五种拒绝策略
AbortPolicy
:抛出RejectedExecutionException 的 RuntimeException异常,可根据业务做重试或做放弃提交等处理。
DiscardPolicy
:直接丢弃任务不做任何提示,存在数据丢失风险。
DiscardOldestPolicy
: 丢弃任务头节点,通常是存活时间最长的任务。给新提交的任务让路,这样也存在一定的数据丢失风险。
CallerRunsPolicy
: 谁提交任务,谁来处理。将任务交给提交任务的线程执行(一般是主线程)。
自定义拒绝策略
,写一个类实现RejectedExecutionHandler
接口
7.有哪些常见的线程池?
-
FixedThreadPool
: 固定线程池。核心线程数由构造参数传入,最大线程数=核心线程数。 -
CachedThreadPool
: 缓存线程池。核心线程数为0,最大线程数为 2^31-1 。 队列的容量为0 (SynchronousQueue
)。 -
ScheduledThreadPool
: 定时线程池。可延迟x秒,可延迟x秒,按y周期执行(起始点有开始或结束)。 -
SingleThreadPool
:单一线程池。和FixedThreadPool
差不多,区别在于只有一个核心线程数。 -
SingleThreadScheduledPool
: 单一定时线程池。和ScheduledThreadPool
差不多,区别在于内部只有一个线程。
8.线程池内部结构
1.线程池管理器
:负责线程创建、销毁、添加任务等;
2.工作线程
: 线程池创建的正在工作的线程;
3.任务队列
( BlockingQueue
):线程满了之后,可以放到任务队列中,起到一定的缓冲;
4.任务
:要求实现统一的接口,方便处理和执行;
9.常见线程池的阻塞队列
-
LinkedBlockingQueue
:容量大小为Integer.MAX_Value
,无界队列。对应线程池有FixedThreaPool
、SingleThreadPool
; -
SynchronousQueue
:容量大小为0。对应线程池有CachedThreadPool
(可理解线程数无限扩展); -
DelayedWorkQueue
: 延迟工作队列。队列中的任务不是按照任务存放的先后顺序放的,而是按照延迟时间的先后存放的。对应线程池有ScheduledThreadPool
、SingleThreadScheduledPool
。
10.为什么不应该自动创建线程池
为什么不应该自动创建线程池,所谓的自动创建线程池就是直接调用 Executors 的各种方法来生成前面了解过的常见的线程池,例如 Executors.newFixedThreadPool()
。但这样做是有一定风险的,接下来我们就来逐一分析自动创建线程池可能带来哪些问题。
public static ExecutorService newFixedThreadPool(int nThreads)
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
FixedThreadPool、SingleThreadPool
:使用的是无界队列(LinkedBlockingQueue
),当任务堆积很多时,会占用大量内存,最终导致OOM。
ChachedTheadPool
:可以无限创建线程(Integer.MAX_VALUE
),任务过多时会导致创建线程达到操作系统上线或者发生OOM
。
ScheduledThreadPool、SingleThreadScheduledPool
:使用的是DelayedWorkQueue队列,实质上也是一种无界队列,会导致OOM。
你可以看到,这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险。
11.合适的线程数量是多少?CPU 核心数和线程数的关系
CPU密集型任务:
占用CPU比较多的任务(加密、解密、计算等),最佳线程数为CPU核心数的 1~2倍。
耗时IO型任务:
IO耗时比较多的任务(数据库、文件读写、网络传输等),占用CPU较少。《Java并发编程实战》推荐:最佳线程数= CPU核心数*(1+线程平均等待时间/线程平均工作时间)。
线程平均工作时间越长,应创建较少的线程。线程平均等待时间长,应创建较多的线程。
12.如何根据实际需要,定制自己的线程池
核心线程数:平均工作时间比例多 ,定义较少的线程数;平均等待时间比例高,创建较多的线程数。如一个任务CPU密集和IO耗时混搭,最大线程数应为核心线程数的几倍,应对突发情况。
阻塞队列: 相对于无界队列,可使用 ArrayBlockingQueue
,可以设置固定容量,防止资源耗尽,同时会产生数据丢失。
另外,队列容量大小和最大线程数应做一个平衡。队列容量大,最大线程数小时,可减少上下文切换,但是减少吞吐量。队列容量小,最大线程数大时,可提高效率,但是增多上下文切换。
线程工厂: 我们可以使用默认的 defaultThreadFactory, 也可以使用 ThreadFactoryBuilder创建 线程工厂,并自定义线程名。
ThreadFactoryBuilder factoryBuilder = new ThreadFacoryBuillder();
ThreadFactory threadFactory = builder.setNameFormat("rpc-pool-%d").build();
这样,线程名会为 rpc-pool-1
,rpc-pool-2
…
拒绝策略:除了4种常规拒绝策略,还可以自定义拒绝策略,做日志打印, 暂存任务、重新执行等操作 。实现方式,继承 RejecedExecutionHandler
接口,重写 rejectedExecution
() 方法。
private static class CustomRejectionHandler implements RejectedExecutionHandler
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
//打印日志、暂存任务、重新执行等拒绝策略
13.如何正确关闭线程池?shutdown 和 shutdownNow 的区别?
-
shutdown()
:调用此方法,线程池不会马上关闭,会等线程运行完 ,并且阻塞的任务运行完再关闭。 -
isShutdown()
:判断线程池是否被标记关闭。调用了shutdown方法后,此方法会返回true。 -
isTerminated()
:判断线程池中是否已关闭并且阻塞的任务都已执行完 。 -
awaitTermination()
:判断线程池终结状态。等待周期内,线程池终结会返回true。超过等待时间,线程池未终结会返回false。等待周期内,线程被中断会抛出InterruptedException异常。 -
shutdownNow()
:表示立即关闭线程池。会向所有线程发送中断信号,并停止线程。将等待中的任务转移到list中,以后可做补救措施。
14.使用队列有什么需要注意的吗
使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。
15.线程只能在任务到达时才启动吗
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread
(启动一个核心线程)或 prestartAllCoreThreads
(启动全部核心线程)方法来提前启动核心线程。
public class Demo10
public static void main(String[] args)
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1));
poolExecutor.prestartCoreThread();
System.out.println(poolExecutor);
16.线程如何回收
线程池回收线程只会发生在当前线程池中线程数量大于corePoolSize
参数的时候;当线程池中线程数量小于等于corePoolSize
参数的时候,回收过程就会停止。
allowCoreThreadTimeOut
设置项可以要求线程池:将包括“核心线程”在内的,没有任务分配的任何线程,在等待keepAliveTime
时间后全部进行回收:
public class Demo10
public static void main(String[] args) throws Exception
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1));
poolExecutor.prestartAllCoreThreads();
poolExecutor.allowCoreThreadTimeOut(true);
System.out.println(poolExecutor);
Thread.sleep(4000);
System.out.println(poolExecutor);
17.线程池状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;//线程池容量
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) return c & ~COUNT_MASK;
private static int workerCountOf(int c) return c & COUNT_MASK;
private static int ctlOf(int rs, int wc) return rs | wc;
18.总结
以上是关于线程池面试汇总的主要内容,如果未能解决你的问题,请参考以下文章