1:线程池介绍
使用线程池,可以复用线程及控制线程的总量。如果不使用线程池,每个任务都要 新开一个线程来处理,反复创建线程开销大,过多的线程会占用太多内存。
这样开销太大了,我们希望有固定数量的线程,来执行这1000个线程,这样就避 免了反复创建并销毁线程所带来的开销问题。
2:使用线程池的好处及场景
1:好处
消除线程创建带来的延迟,加快响应速度。
合理利用CPU和内存
统一管理资源
2:适用场合
服务器接受到大量请求时,使用线程池计数是很适合的,它可以大大减少线程的创 建和销毁的次数,提高服务器的工作效率。
在实际开发中,如果需要创建5个以上的线程,就可以用线程池来管理。
3:线程池参数
1:corePoolSize核心线程数
线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时再创建新线程去执行任务。
2:maxPoolSize最大线程数
线程池可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,就是maxPoolSize
3:新的线程是由ThreadFactory创建
默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,则可以改变线程名、线程组、优先级、是否是守护线程。
4:工作队列类型
直接交换:SynchronousQueue,这个队列没有容量,无法保存工作任务。
无界队列:LinkedBlockingQueue 无界队列
有界队列:ArrayBlockingQueue 有界队列
4:线程池原理
如果线程数小于corePoolSize,即使其它工作线程处于空闲状态,也会创建一个新线程来运行新任务。
如果线程数大于等于corePoolSize,但少于maximumPoolSize,将任务放入队列。
如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务。
如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。
例子:核心池大小为5,最大池大小为10,队列为100
因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。
当队列已满时,将创建最新的线程maxPoolSize,最多到10个线程,如果再来任务,则拒绝。
增减线程的特点
通过设置corePoolSize和maximumPoolSize相同,可以创建固定大小的线程池。
线程池希望保持较少的线程数,并且只有在负载变得很大时才会增加。
通过设置maximumPoolSize为很高的值,例如Integer.MAX_VALUE,可以允许线 程 池容纳任意数量的并发任务。
只有在队列填满时才创建多于corePoolSize的线程,所以如果用的是无界队列 (LinkedBlockingQueue),则线程数就不会超过corePoolSize
5:自动创建线程池
1:newFixedThreadPool
手动创建更好,因为这样可以更明确线程池的运行规则,避免资源耗尽的风险。
threadpool/FixedThreadPoolTest.java
不管有多少任务,始终只有4个线程在执行。
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:演示newFixedThreadPool
*/
public class FixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
源码解读
第一个参数是核心线程数量,第二个参数是最大线程数量,它设置的与核心线程数量一样,不会有超过核心线程数量的线程出现,所以第三个参数存活时间设置为0,第四个参数是无界队列,有再多的任务也都会放在队列中,不会创建新的线程。
如果线程处理任务的速度慢,越来越多的任务就会放在无界队列中,会占用大量内存,这样就会导致内存溢出(OOM)的错误。
threadpool/FixedThreadPoolOOM.java
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述: 演示newFixedThreadPool出错的情况
*/
public class FixedThreadPoolOOM {
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executorService.execute(new SubThread());
}
}
}
class SubThread implements Runnable {
@Override
public void run() {
try {
//处理越慢越好,演示内存溢出
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试时可以将内存设置小些:Run->Run..->Edit Configurations
2:newSingleThreadExecutor
只有一个线程
threadpool/SingleThreadExecutor.java
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
它和newFixedThreadPool的原理相同,只不过把线程数直接设置为1,当请求堆积时,也会占用大量内存。
3: newCachedThreadPool
可缓存线程池,它是无界线程池,并具有自动回收多余线程的功能。
threadpool/CachedThreadPool.java
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
源码解读
SynchronousQueue是直接交换队列,容量为0,所以不能将任务放在队列中。任务过来后直接交给线程去执行。线程池的最大线程数量是整数的最大值,可以认为没有上限,有多个任务过来就创建多个线程去执行。一定时间后(默认60秒),会将多余的线程进行回收。
弊端:第二个参数maximumPoolSize设置为Integer.MAX_VALUE,可能会创建非常多的线程,导致OOM
4:newScheduledThreadPool
支持定时及周期性任务执行的线程池
threadpool/ScheduledThreadPoolTest.java
package threadpool;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
//threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
//先延迟一秒运行,然后每隔3秒运行一次
threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
}
}
6:手动创建线程池
1:正确创建线程池方法
根据不同的业务场景,自己设置线程池的参数,比如内存大小、线程名、任务被拒绝后如何记录日志等。
2:设置线程池数量
CPU密集型(比如加密、计算hash等):最佳线程数为CPU核心数的1-2倍。
耗时IO型(读写数据库、文件、网络读写):最佳线程数应大于CPU核心数很多倍,以JVM线程监控显示繁忙为依据,保证线程空闲时可以衔接上,充分利用CPU
线程数=CPU核心数*(1+平均等待时间/平均工作时间)
7:拒绝策略
1:拒绝时机
1) 当Executor关闭时,提交新任务会被拒绝。
2) 当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时
2:AbortPolicy
直接抛出异常,说明任务没有提交成功
3:DiscardPolicy
线程池会默默的丢弃任务,不会发出通知
4:DiscardOldestPolicy
队列中存有很多任务,将队列中存在时间最久的任务给丢弃。
5:CallerRunsPolicy
当线程池无法处理任务时,那个线程提交任务由那个线程负责运行。好处在于避免丢弃任务和降低提交任务的速度,给线程池一个缓冲时间。
8:线程池满了,往线程池里提交任务会发生什么样的情况
如果你使用的LinkedBlockingQueue(阻塞队列),也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;
如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
线程池的饱和策略:
当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略