面试必问:多线程与线程池

前言

前几章都在讲一些锁的使用和原理,主要是为了保证多线程情况下变量的原子性,但这并不是说多线程不好,合理利用还是有好处的。至于什么好处,看下面内容就懂了,先打个比方吧(谁叫比方,上来挨打):假如你体育考试,要跑1000米,你现在有两个选择:

  • 一个人跑完1000米。
  • 找三个人陪你一起跑,每个人跑250米就好

两种方案你选哪个?

今天写一下面试必问的内容:多线程与线程池。主要从以下几方面来说:

  • 什么是线程(什么是多线程)
  • 线程状态
  • 多线程的优点和弊端
  • 线程池的好处
  • 线程池的新建
  • 线程池状态
  • 线程池执行任务
  • 线程池异常处理
  • 为什么submit()方法提交任务产生异常会被"吞掉"

6月6日,好吉利的数字,祝大家六六大顺,话不多说,开始搞事!

1、什么是线程(什么是多线程)

在这里先说一下进程,什么是进程:昨晚我打开的久违的wegame,登录了英雄联盟客户端,而这个客户端,就是一个进程,客户端进程名是Client.exe,不信你们试试!

线程是操作系统调度的最小单元,每个线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。而多个线程组成了一个进程。一个程序,比如爱奇艺客户端、腾讯客户端、wegam等,至少有一个进程,而一个进程至少有一个线程。

那么问题来了,通过一个main方法启动一个Java程序,这算是进程还是线程呢?代码如下:

public class ThreadTest {

    public static void main(String[] args) {
        
    }
}

答案是:进程,改进一下上述代码,执行如下:

public class ThreadTest {

    public static void main(String[] args) {

        // 获取Java线程管理的MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // dumpAllThreads(boolean lockedMonitors, boolean lockedSynchronizers)
        // 不需要获取同步的synchronizer信息,仅获取线程和线程堆栈晋西
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(truefalse);
        for (ThreadInfo threadInfo : threadInfos){
            System.out.println(
                    "[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()
            );
        }
    }
}
  • main:main线程,程序入口
  • Reference Handler:清除Reference的线程(对象的引用存在虚拟机栈中,GC的时候需要用到)
  • Finalizer:调用对象finalizer方法的线程(GC的时候需要用到)
  • Signal Dispatcher:分发处理发送给JVM信号的线程
  • Monitor Ctrl-Break:同步的monitor线程

看上面例子,能清楚的看到,一个Java程序的运行不仅是main()方法的运行,而是main线程和多个其他的线程同时运行,这就是多线程。

多线程:多线程就是指一个进程中同时有多个执行路径(线程)正在执行

2、线程状态

Java线程在运行的生命周期中可能处于下表所示的6种不同状态,在给定的一个时刻,线程只能处于其中一个状态。

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的成为“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁(这里有个容易混淆的问题,下面会讲)
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断,想下前几篇文章,进入同步队列获取锁的过程,想想有什么问题)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

注意,在上一篇讲ReentrantLock(重入锁)的时候,当已经有线程获取了,其余线程会进入同步队列,尝试自旋几次之后调用LockSupport.park()方法,这个方法是用来阻塞当前线程,那么在调用了这个方法之后,在同步队列中线程的状态是什么呢?BLOCKED还是WAITING?答案是线程处于WAITING(等待状态),阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块时的状态,但是阻塞在Lock接口的线程状态是等待状态。

线程在自身的生命周期中状态变迁图如下所示:

3、多线程的优点和弊端

3.1 优点

  • 多线程技术使程序的响应速度更快(比如打开一个网页,调用一个接口,这个接口创建了很多线程去数据库读取数据异步去返回数据,这样用户立马就打开了网页,里面的内容的展现可能不是一起展示,但是总比空白界面停留十几秒再全部展示出来好吧);

  • 当前没有进行处理的任务时可以将处理器时间让给其它任务;

  • 占用大量处理时间的任务可以定期将处理器时间让给其它任务;

  • 可以随时停止任务(中断某个线程);

  • 可以分别设置各个任务的优先级以优化性能(在线程构建的时候通过setPriority(int)方法来修改优先级,范围为1-10,默认优先级为5,优先级高的线程分配时间片的数量要多于优先级低的线程)。

一个线程在一个时刻只能运行在一个处理器核心上,使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,变得更有效率:

3.2 弊端

  • 等候使用共享资源时造成程序的运行速度变慢(比如库存,如果多个线程同时去扣减,就有可能变成负数,这样是不被允许的,所以就需要之前几篇文章讲的锁来控制,所以前一个线程获取了资源,后一个线程就会被阻塞,造成程序的运行速度变慢)。

  • 对线程进行管理要求额外的CPU开销,线程的使用会给系统带来上下文切换的额外负担(多线程是通过分配CPU时间片来实现的,时间片非常短,所以CPU会不停的切换线程执行,当一个线程执行一个时间片后会切到下一个任务,但是在切换之前会保存上一个任务的状态,下次在切换回这个任务的时候,可以再加载这个任务的状态,这就是上下文切换)。

  • 线程的死锁。即对共享资源加锁实现同步的过程中可能会死锁。

  • 对公有变量的同时读或写,可能对造成脏读等(这就是锁该做的事情)。

  • 线程创建和销毁会造成消耗(这就是线程池该做的事情了)。

4、线程池的好处

  • 降低资源消耗(可以通过重复利用已创建的线程来降低线程创建和销毁造成的消耗)。
  • 提高相应速度(当任务到达时,任务可以不需要等到线程创建就能立即执行)。
  • 提高线程的可管理性(线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控)。

5、线程池的新建

public class ThreadTest {

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
                // handler
                new ThreadPoolExecutor.AbortPolicy());

        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 30; i++){
            threadPoolExecutor.execute(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(5000L);
                }
            });
        }
    }
}
  • corePoolSize:线程池核心线程数大小。
    当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时不再创建。

  • maximumPoolSize:线程池最大线程数量。
    线程池允许创建的最大线程数。如果阻塞队列满了,并且已创建的线程数小于最大线程数,则线程池会在创建新的线程执行任务(如果使用了无界队列,那么这个参数就没什么用了)。

  • keepAliveTime:线程池中非核心线程空闲的存活时间大小

  • unit:线程空闲存活时间单位

  • workQueue:存放任务的阻塞队列
    用于保存等待执行的任务的阻塞队列。可以选择以下几个队列:
    1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
    3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
    4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

  • threadFactory:用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题,也可以设置线程执行出现异常的处理策略(下面文章会讲)
    如上图,每个线程的name都以wx开头。

  • handler:线城池的饱和策略事件,主要有四种类型
    1、AbortPolicy:直接抛出异常,如下图: 2、CallerRunsPolicy:用调用者所在线程来运行任务。
    3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    4、DiscardPolicy:不处理,丢弃掉。

  • 除了上面这四种,还有一种自定义策略,实现RejectedExecutionHandler接口即可:

public class Handler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 打印线程信息
        System.out.println(r.toString());
    }
}

6、线程池状态

线程池的状态主要有以下几种:

状态名称 说明
RUNNING 初始状态,能够接收新任务,以及对已添加的任务进行处理
SHUTDOWN 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务,处理完成之后才会退出。
STOP 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
TIDYING 当所有的任务已终止,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理,可以通过重载terminated()函数来实现。
TERMINATED 线程池执行完钩子函数terminated()之后,就变成TERMINATED状态。

这里有几个要注意的点:

  • 当调用了shutdown()方法之后,就会从RUNNING转变为SHUTDOWN状态,此时不能再向线程池添加新任务,否则将会抛出RejectedExecutionException异常。
  • 当调用了shutdownNow()方法之后,就会从RUNNING转变为STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
  • 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN转变为TIDYING状态。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP转变为TIDYING状态。
图片来源:百度搜索
图片来源:百度搜索

7、线程池执行任务

上面我们创建了一个线程池,并且通过execute()方法提交了任务,然后可以看出上面抛出了异常(拒绝策略), 为什么会这样呢,下面可以看下线程池的处理流程图就明白了:

线程池处理任务流程:

  • 1: 通过判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个线程去执行,如果核心线程都在执行任务。那么就判断阻塞队列。
  • 2: 判断阻塞队列是否已满,如果没满,就将任务加到队列中,如果满了,就判断创建的线程是否达到了最大数量(所以这里有个问题,如果你队列是无界的,那么可以一直往里面添加任务,这就有可能引起内存溢出,这也是阿里官方手册为什么建议用ThreadPoolExecutor去创建线程池了)。
  • 3: ,判断创建的线程是否达到了最大数量,如果没有达到,就创建一个线程去执行任务,如果有达到,就执行拒绝策略(默认的拒绝策略是抛出异常,就上面例子抛出的那个异常)。

接下来看下execute()方法的源代码:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // 获取当前有效的线程数和线程池的状态
        int c = ctl.get();
        // 判断正在运行线程数是否小于核心线程池,是则新创建一个线程执行任务,否则将任务放到任务队列中
        if (workerCountOf(c) < corePoolSize) {
            // 在addWorker中创建工作线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 线程池是否处于运行状态,且是否任务插入任务队列成功
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 线程池是否处于运行状态,如果不是则使刚刚的任务出队
            if (! isRunning(recheck) && remove(command))
                // 执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(nullfalse);
        }
        // 插入队列不成功,且当前线程数数量小于最大线程池数量,此时则创建新线程执行任务,创建失败的话就执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

可以看出,execute()方法是没有返回值的,所以你提交任务之后,是无法判断任务是否被多线程执行成功,所以多线程还有一种提交方式,submit()方法,通过submit()方法提交任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否提交成功,,并且可以通过future.get()来获取返回值,get()方法会阻塞当前线程直到任务完成。实例如下:

public class ThreadTest {

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
                // handler
                new Handler());

        Future<?> submit = null;
        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 10; i++){
            int num = i;
            submit = threadPoolExecutor.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(5000L);
                }
            });
            try {
                submit.get();
            }catch (Exception e){
                System.out.println("线程执行出现异常");
            }
        }
    }
}

线程池异常处理

先来看一段代码:

public class ThreadTest {

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
                // handler
                new Handler());

        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 10; i++){
            int num = i;
            threadPoolExecutor.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    if (num == 4){
                        throw new RuntimeException();
                    }else {
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(5000L);
                    }
                }
            });
        }
    }
}

上述代码可以看到,当i=4的时候,会抛出一个异常,然后看下结果:

本该打印10行结果的,现在只打印了9行,执行报错但是没有抛出异常,这样我们无法感知任务出现了异常,也就无法做相应处理。

但你把上面代码的提交方式改为execute(),再次运行,你会发现有异常抛出的:

这是为啥子呢,怎么解决呢,先来说怎么解决,再说为啥submit()方法提交任务会将其中可能发生的异常掉。解决方法如下:

  • 添加try/catch捕获异常
public class ThreadTest {

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
                // handler
                new Handler());

        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 10; i++){
            int num = i;
            threadPoolExecutor.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    try {
                        if (num == 4){
                            throw new RuntimeException();
                        }else {
                            System.out.println(Thread.currentThread().getName());
                            Thread.sleep(5000L);
                        }
                    }catch (Exception e){
                        System.out.println("线程:" + Thread.currentThread().getName() + "执行任务出现了异常");
                    }
                }
            });
        }
    }
}

查看结果:

  • 利用submit()方法返回的future对象的get()方法来查看程序执行是否有异常产生:
public class ThreadTest {

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
                // handler
                new Handler());

        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 10; i++){
            int num = i;
            Future<?> submit = threadPoolExecutor.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    if (num == 4) {
                        throw new RuntimeException();
                    } else {
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(5000L);
                    }
                }
            });
            try {
                submit.get();
            }catch (Exception e){
                System.out.println("线程:" + Thread.currentThread().getName() + "执行任务出现了异常");
            }
        }
    }
}

查看结果:

你会发现,它不像上面那个try/catch具体到线程池内那个线程出现了问题,而是说你的主线程执行任务出现了异常

  • 还有一种解决方案,这种异常解决方案是execute()方法提交的任务执行出现异常的处理方式,submit()方法提交的不适用。在定义ThreadFactory的时候,调用setUncaughtExceptionHandler()方法来自定义异常处理方式:
public class ThreadTest {

    private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class);

    /**
     * 基于数组的有界阻塞队列
     */

    private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        /**
         * 创建一个线程池
         */

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // corePoolSize
                5,
                // maximumPoolSize
                10,
                // keepAliveTime
                10L,
                // unit
                TimeUnit.SECONDS,
                // workQueue
                arrayBlockingQueue,
                // threadFactory
                new ThreadFactoryBuilder()
                        // 设置线程名称
                        .setNameFormat("wx-%d")
                        // 添加自定义异常处理方式:打印error日志
                        .setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPoolExecutor {} produce exception", thread,throwable))
                        .build(),
                // handler
                new Handler());

        // 上面是创建线程池的代码,下面只是用来测试拒绝策略的
        for (int i = 0; i < 10; i++){
            int num = i;
            threadPoolExecutor.execute(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    if (num == 4) {
                        throw new RuntimeException();
                    } else {
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(5000L);
                    }
                }
            });
        }
    }
}

查看结果:

为什么submit()方法提交任务产生异常会被"吞掉"

说到这个问题,我们得先来看下submit()方法的源码:

public Future<?> submit(Runnable task) {
        if (task == nullthrow new NullPointerException();
        // 任务被包装成RunnableFuture对象,准备添加到工作队列中
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

newTaskFor()方法代码如下:

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

FutureTask类代码如下:

public class FutureTask<Vimplements RunnableFuture<V{
  ......
}

RunnableFuture接口提供了一个run()方法:

public interface RunnableFuture<Vextends RunnableFuture<V{
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */

    void run();
}

看下FutureTask类的run()方法做了什么:

public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    // 捕获异常
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

再看下setException()方法:

protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            // 将异常放入outcome对象中
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

至此,我们可以看到,submit()方法其实是将任务包装成RunnableFuture对象,其实最终是一个FutureTask实例,FutureTask实现了Future和Runnable接口。重写了run(),而在run()方法里面,该任务抛出的异常将被捕获,通过setException()方法将异常放在outcome中,这就是为什么没有抛出异常的原因。

那么问题来了,为什么调用submit()提交任务之后返回的FutureTask对象的get()方法就会看到异常呢,看get()方法源码:

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false0L);
        return report(s);
    }

report()方法代码:

private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

因为get()方法会将存放异常的outcome对象返回出去,这就是为什么调用submit()提交任务之后返回的FutureTask对象的get()方法就会看到异常的原因!

结尾

如果你觉得我的文章对你有帮助话,欢迎关注我的微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各种Java学习资源,期待与你共同进步)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章