线程池面试汇总

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:ExecutorServiceExecutors
	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,无界队列。对应线程池有 FixedThreaPoolSingleThreadPool

  • SynchronousQueue:容量大小为0。对应线程池有CachedThreadPool(可理解线程数无限扩展);

  • DelayedWorkQueue: 延迟工作队列。队列中的任务不是按照任务存放的先后顺序放的,而是按照延迟时间的先后存放的。对应线程池有ScheduledThreadPoolSingleThreadScheduledPool

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-1rpc-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.总结

以上是关于线程池面试汇总的主要内容,如果未能解决你的问题,请参考以下文章

线程池面试汇总

Day288.多线程-面试题汇总 -Juc

面试:多线程容易产生的40个问题汇总

面试汇总

如图两道面试题,顺便深入线程池,并连环17问

抢先准备,40个 Java 多线程面试题及答案大汇总!