线程是一个比较昂贵的资源。
比如线程的创建和启动、线程的销毁、线程调度的开销等
因此,我们需要一种有效使用线程的方式。这就是线程池。
类似线程池这种的对象池(比如数据库连接池),实现方式就是需要的时候,就去池中获取一个对象,用完后还回池中。
线程池节省了不断创建和销毁线程的开销,可以控制线程的数量合理利用CPU资源和内存,并且能够统一管理。
corePoolSize
核心线程数:默认情况下,线程池初始化后,池内没有任何线程,等待任务到来,再创建新线程执行任务
maxPoolSize
最大线程池容量:线程池中线程的最大容量。
keepAliveTime
线程存活时间:当线程池中线程多于corePoolSize时,如果多出的线程的空闲时间超过存活时间,就会回收线程
ThreadFactory
线程工厂:通过工厂模式创建线程,默认使用Executors.defaultThreadFactory(),创建的线程都是非守护线程,都在同一线程组,且都是5的优先级。
workQueue
工作队列:
1.直接交接队列(synchronousQueue):不存储任务,直接中转任务给线程
2.无界队列(LinkedBlockingQueue):无限存储任务,导致maxPoolSize无意义,且不会增加线程数,可能导致OOM
3.有界队列(ArrayBlockingQueue):有限容量,队列满了会判断是否创建新线程。
handler
拒绝任务处理器:当池中最大线程容量和工作队列容量使用有限边界并且饱和时,会拒绝新的任务。
如果一个任务来了,线程池中线程数量少于corePoolSize,则创建新的线程在池中;如果池中线程数量大于或等于corePoolSize且小于maxPoolSize,就将任务放入队列;如果队列满了,线程数量少于maxPoolSize,就创建新的线程;如果线程池数量达到maxPoolSize,且队列也满了,就拒绝任务。
线程池应该手动创建,这样可以自己设置线程池的运行规则,避免资源耗尽的风险
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i <100; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
打印结果:只有三个线程在执行任务.
原因:见源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
只需要传入线程数量,核心线程数和最大线程数相同,并且工作队列是无界的。
当工作队列加入速度大于线程执行速度,会造成大量内存被占用,最终可能导致OOM的发生。
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i <100; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
该方法源码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i <100; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
该方法使用的是直接交接队列,并不存储任务,而最大容量设为整型最大值。每来一个任务就创建一个线程,如果60后有空闲线程,就回收空闲线程。
但是如果线程过多有可能导致OOM
//传入核心线程数
ScheduledExecutorService scheduledExecutorService
= Executors.newScheduledThreadPool(5);
//该方法表示多少时间后执行任务
//传入三个参数,分别传入执行任务,计时时间,时间单位
scheduledExecutorService.schedule(()->{
System.out.println(Thread.currentThread().getName());
},5, TimeUnit.SECONDS);
//该方法表示多少时间后重复执行
//传入四个参数,分别传入执行任务,开始时间,计时时间,时间单位
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName());
},1,5, TimeUnit.SECONDS);
源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
该线程池主要是用于时间相关的。
这四种常见线程池也是各有千秋,都是已经设计好了的,但并不一定是我们想要的,所以为了满足自身需要,我们应该自己手动创建线程池。
根据上文我们得出结论:应该手动创建线程池
那么手动创建线程池则需要考虑到线程池中的线程数量的多少。
如果线程数量少了,会造成CPU的空闲导致CPU资源浪费。而如果线程过多,频繁的上下文切换也会导致性能的开销,反而可能降低程序的效率。所以设置一个线程的数量是很有讲究的。
shutdown()
停止该线程池,执行该方法后,通知线程池该停止了。然后线程池会拒绝新的任务,将线程池内所有任务执行完毕后,停止线程池。如果在此期间再次传入任务,会被拒绝同时抛出异常。
isShutdown()
返回boolean类型,判断当前线程池是否接收到停止命令。
isTerminated
返回boolean类型,判断当前线程池所有任务是否运行完成。
awaitTermination
返回boolean类型,传入两个参数,分别是计时时间和时间单位。这个方法的作用是:判断在传入的时间内,线程是否运行完所有任务。
方法执行后会进入阻塞状态。如果线程池任务执行完了,就立即返回true;如果超时还没执行完,就返回false;如果被打断了会抛出异常。
shutdownNow
该方法比较暴力,向线程池内所有线程发送打断信号,所有的线程会对打断信号进行处理,而队列中剩余的任务会被作为List集合的对象返回。
五种方法的演示:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i <1000; i++) {
executorService.execute(()->{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+"被中断了");
}
System.out.println(Thread.currentThread().getName()+
" "+executorService.isShutdown()+
" "+executorService.isTerminated());
});
}
TimeUnit.SECONDS.sleep(1);
executorService.shutdown();
TimeUnit.SECONDS.sleep(1);
List<Runnable> runnableList = executorService.shutdownNow();
System.out.println("是否运行完?"
+executorService.awaitTermination(10, TimeUnit.SECONDS));
System.out.println("剩余任务数量:"+runnableList.size());
TimeUnit.SECONDS.sleep(1);
System.out.println("是否终止?"+executorService.isTerminated());
}
根据之前谈到的,某些线程池有时是会拒绝新的任务的。
那么线程池何时会拒绝任务呢?
AbortPolicy
中止策略:直接抛出拒绝任务的异常。
DiscardPolicy
丢弃策略:不作任何通知,直接丢弃新的任务
DiscardOldestPolicy
丢弃最老策略:这种策略会丢弃队列中最老的任务,为新任务腾出空间
CallerRunsPolicy
调用者执行策略:线程A传进来这个任务,就让线程A来运行。好处就是保证了业务不会丢失,同时负反馈任务传递速率。
钩子方法可以在任务执行之前和之后调用。可以用来操纵执行环境,记录日志,作统计等。
beforeExecute(Thread t,Runnable r)
在给定的线程中执行给定的Runnable之前调用方法
afterExecute(Thread t,Runnable r)
完成指定Runnable的执行后调用方法
public class HookMethodsThreadPool extends ThreadPoolExecutor {
public HookMethodsThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
System.out.println("开始保存任务日志");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("任务日志保存完毕");
}
public static void main(String[] args) {
HookMethodsThreadPool hookMethodsThreadPool = new HookMethodsThreadPool(3, 5, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue());
for (int i = 0; i <10; i++) {
hookMethodsThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
}
打印结果如下:
...
开始保存任务日志
pool-1-thread-2
任务日志保存完毕
开始保存任务日志
pool-1-thread-1
任务日志保存完毕
...
线程如何做到复用的呢?直接查看核心的源码
每次从工作队列中取出新的任务
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
然后使用while循环,线程不会停止,会不断地去任务队列中获取新的任务
while (task != null || (task = getTask()) != null)
调用task的run方法,而在执行前后有之前提到的钩子方法
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch(...) {
....
} finally {
afterExecute(task, thrown);
}
}