目录
前言
最近java面试,基本都会考察多线程的,多线程就一定要问线程池的,然而我却在同一个问题上栽跟头两次,也是醉醉的。在懊悔之余所以专门花了一个下午的时间把它详细总结整理了一遍,也以此告诫自己学东西切不可浮躁,要静心专研,打扎实基础。
问题:
问:新建线程池有哪几个参数,具体含义是什么呢?
问:假如我设置corePoolSize为2,maximumPoolSize为5,现在线程池里已经有1个了,我再往里面添加第2到6个,具体的执行逻辑是什么呢?
问:常用新建线程池的方法有哪几个,他们与ThreadPoolExecutor有关系吗?
线程池的作用
线程虽然提供了并行处理速度,但是线程的新建销毁带来的系统开销是很大滴,为了能够更科学的利用线程,才有了大名鼎鼎的线程池:ThreadPoolExecutor类。作用大致如下:
1、提高资源利用率
线程池可以重复利用已经创建了的线程,线程任务完成后,可以不销毁而是被安排去做别的任务。
2、具有可管理性
程序员可以设置线程个数,线程池会根据系统的使用情况调整运行的线程,降低系统开销。
简单实例
package thread.threadpool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池的使用
* @author shu jun
*
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
//1. 新建五个线程
Thread ta = new MyThread("thread-a");
Thread tb = new MyThread("thread-b");
Thread tc = new MyThread("thread-c");
Thread td = new MyThread("thread-d");
Thread te = new MyThread("thread-e");
//2. 创建一个可重用固定线程数的线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.AbortPolicy());
//3. 将线程放入池中进行执行
pool.execute(ta);
pool.execute(tb);
pool.execute(tc);
pool.execute(td);
pool.execute(te);
//4. 关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread {
public MyThread(String threadName) {
this.setName(threadName);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ", " + this.getName()+ " is running.");
}
}
输出如下:
参数与原理
ThreadPoolExecutor有4个构造函数,最全的就是上面代码中的七个构造参数,其它构造函数只是默认给出参数而已。
corePoolSize | 核心线程池的线程数量 |
maximumPoolSize | 最大的线程池线程数量 |
keepAliveTime | 空闲线程活动保持时间,要当 线程数>corePoolSize时才起作用。 |
unit | 线程活动保持时间的单位。 |
workQueue | 指定任务队列所使用的阻塞队列 |
ThreadFactory | 线程工厂,用来创建线程 |
RejectedExecutionHandler | 队列已满,而且任务量大于最大线程的异常处理策略 |
线程往线程池里面丢,最后执行调用pool.execute(Thread); excute函数的核心逻辑如下:
Jdk excute源码如下:
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// 获取ctl对应的int值。该int值保存了"线程池中任务的数量"和"线程池状态"信息
int c = ctl.get();
// 1. 当线程池中的任务数量 < "核心池大小"时,即线程池中少于corePoolSize个任务。
// 则通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. 当线程池中的任务数量 >= "核心池大小"时,
// 2.1 而且,"线程池处于允许状态"时,则尝试将任务添加到阻塞队列中。
if (isRunning(c) && workQueue.offer(command)) {
// 再次确认“线程池状态”,若线程池异常终止了,则删除任务;然后通过reject()执行相应的拒绝策略的内容。
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 否则,如果"线程池中任务数量"为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
// 3. 如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
- 这一步很好理解的,如果"线程池中任务数量" < "核心池大小"时,即线程池中少于corePoolSize个任务;此时就新建一个线程,并将该任务添加到线程中进行执行。
- 当线程池中的任务数量 >= "核心池大小"时,那么就跟阻塞队列workQueue相关了。来一个线程就丢到workQueue(这个阻塞队列大小可以自由设置)里面去,如果队列容量很大很大,那就没maximumPoolSize啥事了,一直往里面放就可以了,但是这样是很消耗系统资源滴,所以阻塞队列经常是有界的,如果满了,那就继续第3步。
- 队列满了就去和maximumPoolSize判断,小于等于则创建新线程,大于则不完了,按照拒绝策略RejectedExecutionHandler,执行reject。具体逻辑在这个比较复杂的addWorker函数中,作用可以理解为添加任务到阻塞队列;
private boolean addWorker(Runnable firstTask, boolean core) {
… 此处省略若干字…
for (;;) {
// 获取线程池中任务的数量。
int wc = workerCountOf(c);
// 如果"线程池中任务的数量"超过限制,则返回false。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
…
}
原理基本上就是以上三步了。
剩下的三个参数中keepAliveTime和unit比较好理解,那么ThreadFactory呢?
ThreadFactory是一个线程工厂,应该说的是线程的创建方式吧,默认是用defaultThreadFactory()方法返回DefaultThreadFactory对象,里面用newThread()来创建的线程,这么搞出来的线程对应的任务是Runnable对象,它创建的线程都是“非守护线程”而且“线程优先级都是Thread.NORM_PRIORITY”。
拒绝策略RejectedExecutionHandler具体又分为:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
阻塞队列BlockingQueue还是很有内容的,要总结拓展的话可以挖掘出蛮多好玩的,大致分为三类:
1. 直接提交, SynchronousQueue。
它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。
2. 无界队列, LinkedBlockingQueue。
LinkedBlockingQueue默认为空,则是无界的了,当然也可以传入参数指定线程大小的。
3.有界队列,ArrayBlockingQueue
这又点像LinkedList和ArrayList的感觉,数据存储结构不一样。
线程池的常用创建方式
常见三种方式:
- Executors.newCachedThreadPool():无限线程池。
- Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
- Executors.newSingleThreadExecutor():创建单个线程的线程池。
查看源码发现其实它们调用的都是ThreadPoolExecutor构造函数。
类之间的继承关系如下:
那三个常用方法都是Executors的static方法,而又去调用ThreadPoolExcutor;
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
可以把上面第2小节的例子修改下 , 可以测试不同的线程池新建方式,
//2. 创建一个可重用固定线程数的线程池
/*ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.AbortPolicy());*/
// 无限线程池
//ExecutorService pool = Executors.newCachedThreadPool();
// 创建固定大小的线程池,与上面一开始面创建ThreadPoolExecutor是等效滴
ExecutorService pool = Executors.newFixedThreadPool(2);
// 创建单个线程的线程池
//ExecutorService pool = Executors.newSingleThreadExecutor();
参考链接
JAVA 之ThreadPoolExecutor线程池原理及其execute方法实例详解_怪我咯_php中文网
Java多线程系列--“JUC线程池”03之 线程池原理(二)_skywang12345_cnblogs
线程池BlockingQueue详解_零度anngle_csdn
BlockingQueue的三种实现区别_Men-DD_csdn
线程池真是博大精神,还有不少可拓展研究的,不过主体脉络应该就这些了。