Java面试题之线程池

1、什么是线程池?

线程池,也即是存储多个线程的集合,是一种多线程处理形式。如果每个请求都新创建一个线程来处理,那么当很多请求一块来时服务器资源将会很快耗尽,所以出现线程池这一技术,避免频繁的线程创建和销毁。

2、为什么使用线程池?

线程的创建和销毁是很耗费资源的,这些时间可能会比处理业务的时间还要长,因此,频繁的创建和销毁线程再加上业务逻辑处理时间可能会导致系统资源不足,所以可以考虑剔除频繁创建与销毁线程。

3、使用线程池的好处

①、提前创建好一定数量的线程放在线程池中,当需要的时候就从池中取出,这比需要时临时创建快很多。

②、方便管理。可以编写管理线程池的代码统一管理,假如启动时该程序创建100个线程,当有请求时就从线程池中取出一个线程进行分配其工作,如果请求数大于100,那多出的部分去等待,而不是像不使用线程池那样一直无休止的创建线程导致系统崩溃。

4、常见线程池及使用场景
①、newCacheThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

示例:

public class CacheThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 1; i <= 5; i++){
            final int nowI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000*nowI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + " " + nowI);
                }
            });
        }
    }
}

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

说明:
①、newCacheThreadPool能创建的最大线程数为:Integer.MAX_VALUE;

②、如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果又提交了新的任务,则线程池重新创建一个工作线程。

③、在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

②、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

示例:

public class FixedThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 17; i++){
            final int nowI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " " + nowI);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
//结果为每2秒打印四个数字

说明:
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源

③、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

示例:

public class SingleThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++){
            final int nowI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " " + nowI);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
④、newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

例一:延迟三秒执行

public class ScheduleThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);

        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟三秒");
            }
        },3, TimeUnit.SECONDS);
    }
}

例二:延迟1秒后每三秒执行一次

public class ScheduleThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);

        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟20秒后每三秒执行一次");
            }
        },20,3, TimeUnit.SECONDS);
    }
}
5、线程池原理(ThreadPoolExecutor)

这里使用的是JDK10的ThreadPoolExecutor来学习线程池原理:
在这里插入图片描述
①、Executor接口: 这个接口提供了一种将任务提交与每个任务将如何运行进行了分离,包括线程使用、调度等细节。该接口只定义了一个execute()方法。

execute():将任务提交给线程池,由线程池为该任务创建或者分配线程并启动,其源码如下:
在这里插入图片描述
注意这个方法没有返回值,获取不到线程执行结果

②、ExecutorService:
在这里插入图片描述
提供用于管理终止的方法如 shutDown()和shutDownNow()用于关闭线程池的方法以及判断线程池是否关闭的方法如,isShutdown(),isTerminated()的方法。

shutdownNow: 对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。

shutdown: 当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

提供了可以生成用于跟踪一个或多个异步任务进度的方法如,invokeAll(),submit()。这些方法的返回值都是Future类型,可以获取线程的执行结果。
在这里插入图片描述
③、ThreadPoolExecutor:
ThreadPoolExecutor的部分常量与函数如下:
在这里插入图片描述
ctl是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,ctl是一个Integer, 它包含两部分的信息: 高三位表示线程池的运行状态 (runState) 和低29位表示线程池内有效线程的数量 (workerCount),

线程池的生命周期,总共有五种状态

RUNNING : 能接受新提交的任务,并且也能处理阻塞队列中的任务;

SHUTDOWN: 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);

STOP: 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;

TIDYING: 如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。

TERMINATED: 在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

线程池生命周期如下图:
在这里插入图片描述
上图中关于ctl的三个方法做如下解释:
在这里插入图片描述
runStateOf:获取运行状态;

workerCountOf:获取活动线程数;

ctlOf:获取运行状态和活动线程数的值

ThreadPoolExecutor的构造函数:
在这里插入图片描述
corePoolSize: 核心线程数量,当有新任务在execute()方法提交时,会执行以下判断:

a):如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;

b):如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,当workQueue未满的时候任务添加到workQueue中,当workQueue满时才创建新的线程去处理任务;

c):如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;

d):如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;

所以,任务提交时,判断的顺序为 corePoolSize –> workQueue –> maximumPoolSize。

maximumPoolSize: 最大线程数量;

workQueue: 等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;

workQueue: 保存等待执行的任务的阻塞队列,当提交一个新的任务到线程池以后, 线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式,主要有以下几种处理方式:

直接切换:这种方式常用的队列是SynchronousQueue。它是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

使用无界队列: 一般使用基于链表的阻塞队列LinkedBlockingQueue。如果使用这种方式,那么线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了。当线程池中所有的核心线程都是RUNNING状态时,这时一个新的任务提交就会放入等待队列中。它是一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

使用有界队列: 一般使用ArrayBlockingQueue。使用该方式可以将线程池的最大线程数量限制为maximumPoolSize,这样能够降低资源的消耗,但同时这种方式也使得线程池对线程的调度变得更困难,因为线程池和队列的容量都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个数量。

如果要想降低系统资源的消耗(包括CPU的使用率,操作系统资源的消耗,上下文环境切换的开销等), 可以设置较大的队列容量和较小的线程池容量, 但这样也会降低线程处理任务的吞吐量。

如果提交的任务经常发生阻塞,那么可以考虑通过调用 setMaximumPoolSize() 方法来重新设定线程池的容量。

如果队列的容量设置的较小,通常需要将线程池的容量设置大一点,这样CPU的使用率会相对的高一些。但如果线程池的容量设置的过大,则在提交的任务数量太多的情况下,并发量会增加,那么线程之间的调度就是一个要考虑的问题,因为这样反而有可能降低处理任务的吞吐量。

keepAliveTime: 线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

threadFactory: 它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

handler: 它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供了4种策略:

AbortPolicy: 直接抛出异常,这是默认策略;

CallerRunsPolicy: 用调用者所在的线程来执行任务;

DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;

DiscardPolicy: 直接丢弃任务;

线程池的流程:
在这里插入图片描述

线程池中的核心线程和非核心线程,没有什么区别,都是线程,只不过人为的规则线程池中的一部分线程叫核心线程

在这里插入图片描述
ThreadPoolExecutor执行execute方法分下面4种情况。

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用

RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

6、为什么线程池中的线程可以重复利用?

①、execute()方法中调用了一个addWork()方法,在execute()方法中使用addWorker()方法的地方只有在添加核心线程和非核心线程的时候调用。在execute()方法中并没有实现线程池是如何维护线程不被销毁,从而达到重复利用的。

②、查看addWork()方法,可以看到,向线程池中添加的Runnable被包装成Worker对象,并调用了start()方法。

③、看下Worker类:
在这里插入图片描述
Worker类继承Runnable,和AbstractQueuedSynchronizer
查看run()方法,调用runWorker,并将自身作为参数:
在这里插入图片描述
④、查看runWorker(),在前面的addWorker()方法在最后是执行了start()方法,也就是Worker的run()方法,进而执行了runWorker()方法。
runWorker方法代码如下:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    try {
                        task.run();
                        afterExecute(task, null);
                    } catch (Throwable ex) {
                        afterExecute(task, ex);
                        throw ex;
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

这个while就是线程池中线程不被销毁的原因所在,在Worker的run方法中,如果while一直执行下去,那么Worker这个继承了Runnable接口的线程就会一直执行下去,而我们知道线程池中任务的载体是Worker,如果Worker一直执行下去,就表示该载体可以一直存在,换的只是载体上我们通过execute()方法添加的Runnable任务而已。

看一下while循环的条件:
在这里插入图片描述
⑤、调用getTask()方法确保while循环执行,在第一次执行完while后task设置为null,那么就要保证task=getTask()!=null,查看getTask(),从名字可以看出这是获取一个任务。

参考文章:
Java线程池原理
java线程池 面试题(精简)
Java线程池(二)
java常用的几种线程池比较

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值