别以为线程池很简单,来回答下这些问题,阿里云Java面试

new NamedThreadFactory(“demo-work”)




它的实现还是很巧妙的,有兴趣地可以看看它的源码,每调用一次,底层有个计数器会加一,会依次命名为 「demo-work-thread-1」, 「demo-work-thread-2」, 「demo-work-thread-3」这样递增的字符串。



在实际的业务场景中,一般很难确定 corePoolSize, workQueue,maximumPoolSize 的大小,如果出问题了,一般来说只能重新设置一下这些参数再发布,这样往往需要耗费一些时间,美团的这篇文章给出了让人眼前一亮的解决方案,当发现问题(线程池监控告警)时,动态调整这些参数,可以让这些参数实时生效,能在发现问题时及时解决,确实是个很好的思路。



> 另外小编整理收藏了20年多家公司面试知识点整理 ,以及各种Java核心知识点免费分享给大家,下方只是部分截图,想要资料的话可以[点击直接进入,免费获取!](https://gitee.com/vip204888/java-p7)暗号:CSDN



![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110145538894.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)



[](https://gitee.com/vip204888/java-p7)线程池提交任务的两种方式

-------------------------------------------------------------------------------



线程池创建好了,该怎么给它提交任务,有两种方式,调用 execute 和 submit 方法,来看下这两个方法的方法签名



// 方式一:execute 方法

public void execute(Runnable command) {

}

// 方式二:ExecutorService 中 submit 的三个方法

Future submit(Callable task);

Future submit(Runnable task, T result);

Future<?> submit(Runnable task);




区别在于调用 execute 无返回值,而调用 submit 可以返回 Future,那么这个 Future 能到底能干啥呢,看它的接口



public interface Future {

/**

 * 取消正在执行的任务,如果任务已执行或已被取消,或者由于某些原因不能取消则返回 false

 * 如果任务未开始或者任务已开始但可以中断(mayInterruptIfRunning 为 true),则

 * 可以取消/中断此任务

 */

boolean cancel(boolean mayInterruptIfRunning);



/**

 * 任务在完成前是否已被取消

 */

boolean isCancelled();



/**

 * 正常的执行完流程流程,或抛出异常,或取消导致的任务完成都会返回 true

 */

boolean isDone();



/**

 * 阻塞等待任务的执行结果

 */

V get() throws InterruptedException, ExecutionException;



/**

 * 阻塞等待任务的执行结果,不过这里指定了时间,如果在 timeout 时间内任务还未执行完成,

 * 则抛出 TimeoutException 异常

 */

V get(long timeout, TimeUnit unit)

    throws InterruptedException, ExecutionException, TimeoutException;

}




可以用 Future 取消任务,判断任务是否已取消/完成,甚至可以阻塞等待结果。



submit 为啥能提交任务(Runnable)的同时也能返回任务(Future)的执行结果呢  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144026545.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)  

原来在最后执行 execute 前用 newTaskFor 将 task 封装成了 RunnableFuture,newTaskFor 返回了 FutureTask 这个类,结构图如下  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144037455.png#pic_center)  

可以看到 FutureTask 这个接口既实现了 Runnable 接口,也实现 Future 接口,所以在提交任务的同时也能利用 Future 接口来执行任务的取消,获取任务的状态,等待执行结果这些操作。  

execute 与 submit 除了是否能返回执行结果这一区别外,还有一个重要区别,那就是使用 execute 执行如果发生了异常,是捕获不到的,默认会执行 ThreadGroup 的 uncaughtException 方法(下图数字 2 对应的逻辑)  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144050405.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)  

所以如果你想监控执行 execute 方法时发生的异常,需要通过 threadFactory 来指定一个 UncaughtExceptionHandler,这样就会执行上图中的 1,进而执行 UncaughtExceptionHandler 中的逻辑,如下所示:



//1.实现一个自己的线程池工厂

ThreadFactory factory = (Runnable r) -> {

//创建一个线程

Thread t = new Thread(r);

//给创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑

t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {

    // 在此设置统计监控逻辑

    System.out.println("线程工厂设置的exceptionHandler" + e.getMessage());

});

return t;

};

// 2.创建一个自己定义的线程池,使用自己定义的线程工厂

ExecutorService service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10),factory);

//3.提交任务

service.execute(()->{

int i=1/0;

});




执行以上逻辑最终会输出「线程工厂设置的exceptionHandler/ by zero」,通过这样的方式就能通过设定的 defaultUncaughtExceptionHandler 来执行我们的监控逻辑了



如果用 submit ,如何捕获异常呢,当我们调用 future.get 就可以捕获



Callable testCallable = xxx;

Future future = executor.submit(myCallable);

try {

future1.get(3));

} catch (InterruptedException e) {

e.printStackTrace();

} catch (ExecutionException e) {

e.printStackTrace();

}




那么 future 为啥在 get 的时候才捕获异步呢,因为在执行 submit 时抛出异常后此异常被保存了起来,而在 get 的时候才被抛出  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144135105.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)  

关于 execute 和 submit 的执行流程 why 神的这篇文章写得非常透彻,我就不拾人牙慧了,建议大家好好品品,收获会很大!



[](https://gitee.com/vip204888/java-p7)ThreadPoolExecutor 源码剖析

------------------------------------------------------------------------------------------



前面铺垫了这么多,终于到了最核心的源码剖析环节了。



对于线程池来说,我们最关心的是它的「状态」和「可运行的线程数量」,一般来说我们可以选择用两个变量来记录,不过 Doug Lea 只用了一个变量(ctl)就达成目的了,我们知道变量越多,代码的可维护性就越差,也越容易出 bug, 所以只用一个变量就达成了两个变量的效果,这让代码的可维护性大大提高,那么他是怎么设计的呢



// ThreadPoolExecutor.java

public class ThreadPoolExecutor extends AbstractExecutorService {

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static final int COUNT_BITS = Integer.SIZE - 3;

private static final int CAPACITY   = (1 << COUNT_BITS) - 1;



// 结果: 111 00000000000000000000000000000

private static final int RUNNING    = -1 << COUNT_BITS;

// 结果: 000 00000000000000000000000000000

private static final int SHUTDOWN   =  0 << COUNT_BITS;

// 结果: 001 00000000000000000000000000000

private static final int STOP       =  1 << COUNT_BITS;

// 结果: 010 00000000000000000000000000000

private static final int TIDYING    =  2 << COUNT_BITS;

// 结果: 011 00000000000000000000000000000

private static final int TERMINATED =  3 << COUNT_BITS;



// 获取线程池的状态

private static int runStateOf(int c)     { return c & ~CAPACITY; }

// 获取线程数量

private static int workerCountOf(int c)  { return c & CAPACITY; }

}




可以看到,ctl 是一个 原子类的 Integer 变量,有 32 位,低 29 位表示线程数量, 29 位最大可以表示 (2^29)-1 (大概 5 亿多),足够记录线程大小了,如果未来还是不够,可以把 ctl 声明为 AtomicLong,高 3 位用来表示线程池的状态,3 位可以表示 8 个线程池的状态,由于线程池总共只有五个状态,所以 3 位也是足够了,线程池的五个状态如下



*   RUNNING: 接收新的任务,并能继续处理 workQueue 中的任务

*   SHUTDOWN: 不再接收新的任务,不过能继续处理 workQueue 中的任务

*   STOP: 不再接收新的任务,也不再处理 workQueue 中的任务,并且会中断正在处理任务的线程

*   TIDYING: 所有的任务都完结了,并且线程数量(workCount)为 0 时即为此状态,进入此状态后会调用 terminated() 这个钩子方法进入 TERMINATED 状态

*   TERMINATED: 调用 terminated() 方法后即为此状态



线程池的状态流转及触发条件如下  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144303895.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)



有了这些基础,我们来分析下 execute 的源码



public void execute(Runnable command) {

if (command == null)

    throw new NullPointerException();

int c = ctl.get();

// 如果当前线程数少于核心线程数(corePoolSize),无论核心线程是否忙碌,都创建线程,直到达到 corePoolSize 为止

if (workerCountOf(c) < corePoolSize) {

    // 创建线程并将此任务交给 worker 处理(此时此任务即 worker 中的 firstTask)

    if (addWorker(command, true))

        return;

    c = ctl.get();

}



// 如果线程池处于 RUNNING 状态,并且线程数大于 corePoolSize 或者 

// 线程数少于 corePoolSize 但创建线程失败了,则将任务丢进 workQueue 中

if (isRunning(c) && workQueue.offer(command)) {

    int recheck = ctl.get();

    // 这里需要再次检查线程池是否处于 RUNNING 状态,因为在任务入队后可能线程池状态会发生变化,(比如调用了 shutdown 方法等),如果线程状态发生变化了,则移除此任务,执行拒绝策略

    if (! isRunning(recheck) && remove(command))

        reject(command);

    // 如果线程池在 RUNNING 状态下,线程数为 0,则新建线程加速处理 workQueue 中的任务

    else if (workerCountOf(recheck) == 0)

        addWorker(null, false);

}

// 这段逻辑说明线程数大于 corePoolSize 且任务入队失败了,此时会以最大线程数(maximumPoolSize)为界来创建线程,如果失败,说明线程数超过了 maximumPoolSize,则执行拒绝策略

else if (!addWorker(command, false))

    reject(command);

}




从这段代码中可以看到,创建线程是调用 addWorker 实现的,在分析 addWorker 之前,有必要简单提一下 Worker,线程池把每一个执行任务的线程都封装为 Worker 的形式,取名为 Worker 很形象,线程池的本质是生产者-消费者模型,生产者不断地往 workQueue 中丢 task, workQueue 就像流水线一样不断地输送着任务,而 worker(工人) 不断地取任务来执行  

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201110144328145.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80OTUyNzMzNA==,size_16,color_FFFFFF,t_70#pic_center)  

那么问题来了,为啥要把线程封装到 worker 中呢,线程池拿到 task 后直接丢给线程处理或者让线程自己去 workQueue 中处理不就完了?  

将线程封装为 worker 主要是为了更好地管理线程的中断



来看下 Worker 的定义



// 此处可以看出 worker 既是一个 Runnable 任务,也实现了 AQS(实际上是用 AQS 实现了一个独占锁,这样由于 worker 运行时会上锁,执行 shutdown,setCorePoolSize,setMaximumPoolSize等方法时会试着中断线程(interruptIdleWorkers) ,在这个方法中断方法中会先尝试获取 worker 的锁,如果不成功,说明 worker 在运行中,此时会先让 worker 执行完任务再关闭 worker 的线程,实现优雅关闭线程的目的)

private final class Worker

extends AbstractQueuedSynchronizer

implements Runnable

{

    private static final long serialVersionUID = 6138294804551838833L;



    // 实际执行任务的线程

    final Thread thread;

    // 上文提到,如果当前线程数少于核心线程数,创建线程并将提交的任务交给 worker 处理处理,此时 firstTask 即为此提交的任务,如果 worker 从 workQueue 中获取任务,则 firstTask 为空

    Runnable firstTask;

    // 统计完成的任务数

    volatile long completedTasks;



    Worker(Runnable firstTask) {

        // 初始化为 -1,这样在线程运行前(调用runWorker)禁止中断,在 interruptIfStarted() 方法中会判断 getState()>=0

        setState(-1); 

        this.firstTask = firstTask;



        // 根据线程池的 threadFactory 创建一个线程,将 worker 本身传给线程(因为 worker 实现了 Runnable 接口)

        this.thread = getThreadFactory().newThread(this);

    }



    public void run() {

        // thread 启动后会调用此方法

        runWorker(this);

    }



   

    // 1 代表被锁住了,0 代表未锁

    protected boolean isHeldExclusively() {

        return getState() != 0;

    }



    // 尝试获取锁

    protected boolean tryAcquire(int unused) {

        // 从这里可以看出它是一个独占锁,因为当获取锁后,cas 设置 state 不可能成功,这里我们也能明白上文中将 state 设置为 -1 的作用,这种情况下永远不可能获取得锁,而 worker 要被中断首先必须获取锁

        if (compareAndSetState(0, 1)) {

            setExclusiveOwnerThread(Thread.currentThread());

            return true;

        }

        return false;

    }



    // 尝试释放锁

    protected boolean tryRelease(int unused) {

        setExclusiveOwnerThread(null);

        setState(0);

        return true;

    }    



    public void lock()        { acquire(1); }

    public boolean tryLock()  { return tryAcquire(1); }

    public void unlock()      { release(1); }

    public boolean isLocked() { return isHeldExclusively(); }

        

    // 中断线程,这个方法会被 shutdowNow 调用,从中可以看出 shutdownNow 要中断线程不需要获取锁,也就是说如果线程正在运行,照样会给你中断掉,所以一般来说我们不用 shutdowNow 来中断线程,太粗暴了,中断时线程很可能在执行任务,影响任务执行

    void interruptIfStarted() {

        Thread t;

        // 中断也是有条件的,必须是 state >= 0 且 t != null 且线程未被中断

        // 如果 state == -1 ,不执行中断,再次明白了为啥上文中 setState(-1) 的意义

        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {

            try {

                t.interrupt();

            } catch (SecurityException ignore) {

            }

        }

    }

} 



通过上文对 Worker 类的分析,相信大家不难理解 将线程封装为 worker 主要是为了更好地管理线程的中断 这句话。



理解了 Worker 的意义,我们再来看 addWorker 的方法



private boolean addWorker(Runnable firstTask, boolean core) {

retry:

for (;;) {

    int c = ctl.get();



    // 获取线程池的状态

    int rs = runStateOf(c);



    // 如果线程池的状态 >= SHUTDOWN,即为 SHUTDOWN,STOP,TIDYING,TERMINATED 这四个状态,只有一种情况有可能创建线程,即线程状态为 SHUTDOWN, 且队列非空时,firstTask == null 代表创建一个不接收新任务的线程(此线程会从 workQueue 中获取任务再执行),这种情况下创建线程是为了加速处理完 workQueue 中的任务

    if (rs >= SHUTDOWN &&

        ! (rs == SHUTDOWN &&

           firstTask == null &&

           ! workQueue.isEmpty()))

        return false;



    for (;;) {

        // 获取线程数

        int wc = workerCountOf(c);

        // 如果超过了线程池的最大 CAPACITY(5 亿多,基本不可能)

        // 或者 超过了 corePoolSize(core 为 true) 或者 maximumPoolSize(core 为 false) 时

        // 则返回 false

        if (wc >= CAPACITY ||

            wc >= (core ? corePoolSize : maximumPoolSize))

            return false;

        // 否则 CAS 增加线程的数量,如果成功跳出双重循环

        if (compareAndIncrementWorkerCount(c))

            break retry;

        c = ctl.get();  // Re-read ctl



        // 如果线程运行状态发生变化,跳到外层循环继续执行

        if (runStateOf(c) != rs)

            continue retry;

        // 说明是因为 CAS 增加线程数量失败所致,继续执行 retry 的内层循环

    }

}



boolean workerStarted = false;

boolean workerAdded = false;

Worker w = null;

try {

    // 能执行到这里,说明满足增加 worker 的条件了,所以创建 worker,准备添加进线程池中执行任务

    w = new Worker(firstTask);

    final Thread t = w.thread;

    if (t != null) {

        // 加锁,是因为下文要把 w 添加进 workers 中, workers 是 HashSet,不是线程安全的,所以需要加锁予以保证

        final ReentrantLock mainLock = this.mainLock;

        mainLock.lock();

        try {

            //  再次 check 线程池的状态以防执行到此步时发生中断等

            int rs = runStateOf(ctl.get());

            // 如果线程池状态小于 SHUTDOWN(即为 RUNNING),

            // 或者状态为 SHUTDOWN 但 firstTask == null(代表不接收任务,只是创建线程处理 workQueue 中的任务),则满足添加 worker 的条件

            if (rs < SHUTDOWN ||

                (rs == SHUTDOWN && firstTask == null)) {

                                    // 如果线程已启动,显然有问题(因为创建 worker 后,还没启动线程呢),抛出异常

                if (t.isAlive()) 

                    throw new IllegalThreadStateException();

                workers.add(w);

                int s = workers.size();



                // 记录最大的线程池大小以作监控之用

                if (s > largestPoolSize)

                    largestPoolSize = s;

                workerAdded = true;

            }

        } finally {

            mainLock.unlock();

        }



        // 说明往 workers 中添加 worker 成功,此时启动线程

        if (workerAdded) {

            t.start();

            workerStarted = true;

        }

    }

} finally {

    // 添加线程失败,执行 addWorkerFailed 方法,主要做了将 worker 从 workers 中移除,减少线程数,并尝试着关闭线程池这样的操作

    if (! workerStarted)

        addWorkerFailed(w);

}

return workerStarted;

}




从这段代码我们可以看到多线程下情况的不可预料性,我们发现在满足条件情况下,又对线程状态重新进行了 check,以防期间出现中断等线程池状态发生变更的操作,这也给我们以启发:多线程环境下的各种临界条件一定要考虑到位。



执行 addWorker 创建 worker 成功后,线程开始执行了(t.start()),由于在创建 Worker 时,将 Worker 自己传给了此线程,所以启动线程后,会调用 Worker 的 run 方法



public void run() {

runWorker(this);

}




可以看到最终会调用 runWorker 方法,接下来我们来分析下 runWorker 方法



final void runWorker(Worker w) {

Thread wt = Thread.currentThread();

Runnable task = w.firstTask;

w.firstTask = null;

// unlock 会调用 tryRelease 方法将 state 设置成 0,代表允许中断,允许中断的条件上文我们在 interruptIfStarted() 中有提过,即 state >= 0

w.unlock();

boolean completedAbruptly = true;

try {

    // 如果在提交任务时创建了线程,并把任务丢给此线程,则会先执行此 task

    // 否则从任务队列中获取 task 来执行(即 getTask() 方法)

    while (task != null || (task = getTask()) != null) {

        w.lock();

        

        // 如果线程池状态为 >= STOP(即 STOP,TIDYING,TERMINATED )时,则线程应该中断

        // 如果线程池状态 < STOP, 线程不应该中断,如果中断了(Thread.interrupted() 返回 true,并清除标志位),再次判断线程池状态(防止在清除标志位时执行了 shutdownNow() 这样的方法),如果此时线程池为 STOP,执行线程中断

        if ((runStateAtLeast(ctl.get(), STOP) ||

             (Thread.interrupted() &&

              runStateAtLeast(ctl.get(), STOP))) &&

            !wt.isInterrupted())

            wt.interrupt();

        try {

            // 执行任务前,子类可实现此钩子方法作为统计之用

            beforeExecute(wt, task);

            Throwable thrown = null;

            try {

                task.run();

            } catch (RuntimeException x) {

                thrown = x; throw x;

            } catch (Error x) {

                thrown = x; throw x;

            } catch (Throwable x) {

                thrown = x; throw new Error(x);

            } finally {

                // 执行任务后,子类可实现此钩子方法作为统计之用

                afterExecute(task, thrown);

            }

        } finally {

            task = null;

            w.completedTasks++;

            w.unlock();

        }

    }

    completedAbruptly = false;

} finally {

    // 如果执行到这只有两种可能,一种是执行过程中异常中断了,一种是队列里没有任务了,从这里可以看出线程没有核心线程与非核心线程之分,哪个任务异常了或者正常退出了都会执行此方法,此方法会根据情况将线程数-1

    processWorkerExit(w, completedAbruptly);

}

}






# **读者福利**

**由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴**

**关注+点赞后,[点击这里获取](https://gitee.com/vip204888/java-p7)完整面试题(含答案)!**

![35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」](https://img-blog.csdnimg.cn/img_convert/12c1b3f07a15f1c5723bb7db0544c5dd.png)

![35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」](https://img-blog.csdnimg.cn/img_convert/a836a9cef573d48df44c616840bd4242.png)

![35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」](https://img-blog.csdnimg.cn/img_convert/e49b163a961b807a32da0fa15742c81a.png)

**更多笔记分享**

 thrown = x; throw new Error(x);

                } finally {

                    // 执行任务后,子类可实现此钩子方法作为统计之用

                    afterExecute(task, thrown);

                }

            } finally {

                task = null;

                w.completedTasks++;

                w.unlock();

            }

        }

        completedAbruptly = false;

    } finally {

        // 如果执行到这只有两种可能,一种是执行过程中异常中断了,一种是队列里没有任务了,从这里可以看出线程没有核心线程与非核心线程之分,哪个任务异常了或者正常退出了都会执行此方法,此方法会根据情况将线程数-1

        processWorkerExit(w, completedAbruptly);

    }

} 

读者福利

由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴

关注+点赞后,点击这里获取完整面试题(含答案)!

[外链图片转存中…(img-Vm2OamUW-1628436356595)]

[外链图片转存中…(img-vjtdMriC-1628436356598)]

[外链图片转存中…(img-VMpV5NKq-1628436356600)]

更多笔记分享

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值