juc线程池最重要的接口是ExecutorService,ThreadPoolExecutor类是线程池的具体实现
一、线程池状态
ThreadPoolExecutor中使用int的高3位来表示线程池的状态,低29位表示线程数量
状态名 | 高3位 | 接受新任务 | 处理阻塞队列任务 | 说明 |
Running | 111 | Y | Y | |
ShutDown | 000 | N | Y | 不会接受新任务,但是会处理阻塞队列的剩余任务 |
stop | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列的任务 |
Tidying | 010 | 任务全部执行完成,活动线程数为0 | ||
Terminated | 011 | 终结状态 |
Terminated>Tidying>stop>ShutDown>Running
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
线程池使用一个32位整型来表示两个状态,使用一次CAS操作即可修改当前两个状态,如果此种设计,定义两个状态,使用两次CAS操作,有可能导致数据不一致。
二、线程池参数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//存活时间(针对救急线程,救急线程=最大线程数-核心线程数)
TimeUnit unit,//存活时间的单位(针对救急线程)
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//产生线程的工厂
RejectedExecutionHandler handler//当核心线程繁忙,阻塞队列满了,救急线程也繁忙,此时执行拒绝策略
)
1、线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务
2、当线程数达到corePoolSize并没有线程空闲,这是再加入任务,新加的任务会被加入workQueue队列中排队,直到有空闲的线程
3、如果队列选择了有界队列,那么任务超过了队列大小时,会创建MaxPoolSize-corePoolSize数量的救急线程来执行任务
4、如果线程达到最大线程数,仍然有新任务加入,此时执行拒绝策略(jdk提供了4种,第三方框架也有提供)
(1)AbortPolicy(调用者抛出RejectedExecutionException异常,这是默认策略)
(2)CallerRunsPolicy(调用者运行任务)
(3)DiscardPolicy(放弃本次任务)
(4)DiscardOldestPolicy(放弃队列中最早的任务,用当前任务替代)
(5)Netty的实现是创建一个新的线程来执行任务
5、当高峰过去,超过corePoolSize的救急线程如果过了指定的一段时间(keepAliveTime和TimeUnit 指定)没有任务执行,将会被回收。
三、常用的工厂线程池
1、newFixedThreadPool(适用于任务量已知,相对耗时的任务)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点
(1)核心线程数和最大线程数一致,即没有救急线程
(2)阻塞队列是无界的,可以放任意数量的任务
2、newCachedThreadPool(适用于任务数密集,但每个任务执行时间较短)
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点
(1)核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,即全部都是救急线程(60s后无任务执行就回收)
(2)阻塞队列采用SynchronousQueue,它没有容量,没有线程来取的话,放入任务的线程是放不进去的处于阻塞状态(类似生活中一手交钱,一手交货)
3、newSingleThreadExecutor(适用于多个任务排队执行)
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特点
(1)核心线程数为1,最大线程数也为1,即没有救急线程
四、提交任务
1、execute(执行任务没有返回值)
void execute(Runnable command);
2、submit(提交任务,用Future获取任务执行结果)
<T> Future<T> submit(Callable<T> task);
3、invokeAll(提交tasks所有任务)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
4、invokeAll(提交所有任务,且带超时时间)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
5、invokeAny(提交tasks所有任务,tasks任意一个任务执行完成,返回此任务的执行结果,其他任务取消)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
6、invokeAny(提交tasks所有任务,tasks任意一个任务执行完成,返回此任务的执行结果,其他任务取消,带超时时间)
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
五、关闭线程池
ThreadPoolExecutor类的关闭方法
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//修改线程状态
advanceRunState(SHUTDOWN);
//打断空闲线程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//尝试终结(没有运行的线程可以立即终结,还有运行的线程不会等待)
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//修改线程池状态
advanceRunState(STOP);
//打断所有线程
interruptWorkers();
//获取队列中剩余任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//尝试终结
tryTerminate();
return tasks;
}
六、工作线程
有限的工作线程轮流异步处理无限的任务,典型实现就是线程池,同时也体现了设计模式里的享元模式。
1、饥饿
固定大小线程池会有饥饿现象
举例
两个工人是同一个线程池中两个线程,这两个线程的要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
(1)客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
(2)后厨做菜:做菜
场景1:来了一位客人。工人A处理点餐任务,工人B做菜,相安无事
场景2:同时来了两位客人,此时工人A和工人B都处理点餐了,此时没人做菜,导致饥饿现象
public class Test { private static List<String> menu= Arrays.asList(new String[]{"地三鲜","辣子鸡丁","烤鸡翅"}); private static Random random=new Random(); public static void main(String[] args) throws Exception{ ExecutorService pool = Executors.newFixedThreadPool(2); pool.execute(()->{ System.out.println("处理点餐"); Future<String> future = pool.submit(() -> { System.out.println("做菜"); return cooking(); }); try { String s = future.get(); System.out.println("上菜,"+s); }catch (Exception e){ e.printStackTrace(); } }); pool.execute(()->{ System.out.println("处理点餐"); Future<String> future = pool.submit(() -> { System.out.println("做菜"); return cooking(); }); try { String s = future.get(); System.out.println("上菜"); }catch (Exception e){ e.printStackTrace(); } }); } private static String cooking(){ return menu.get(random.nextInt(menu.size())); } }
运行结果
此时线程池处于饥饿状态了
解决饥饿现象,不能通过增加线程数来解决,最好的解决方式是针对不同的任务类型使用不同的线程池,这样就可以避免饥饿,并提升效率
2、选择合适的线程数
线程数过小,不能充分利用资源,过大会导致线程上下文的切换,占用更多的内存
(1)cpu密集型运算
采用cpu核数+1实现最优的cpu利用率,+1的操作是保证当线程由于页缺失故障或者其他原因导致暂停时,额外的这个线程就可以使用,保证cpu时钟周期不被浪费
(2)IO密集型运算
cpu不总是处于繁忙状态,当执行业务计算时,需要使用cpu资源,但是执行IO操作,远程RPC调用时,数据操作时,cpu时空闲的,此时可以使用多线程提高利用率
线程数=cpu核数*期望cpu利用率*总时间(cpu计算时间+等待时间)/cpu计算时间
七、任务调度线程池
1、Timer
java.util.Timer可以实现定时功能
优点,简单易用,当然缺点很严重
有如下缺点
(1)所有任务都是同一个线程来调度,因此所有任务都是串行的,同一时间只能有一个任务在执行
(2)前一个任务延迟,后面所有任务都延迟
(3)前一个任务由于异常导致失败,后面任务都不执行
2、ScheduledExecutorService
ScheduledExecutorService解决了Timer的缺点问题
(1)scheduleAtFixedRate
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,//初始延迟时间 long period,//两个任务的间隔 TimeUnit unit);//单位
假设period=1,unit为秒,如果每个任务执行完成都很快,那么每个任务的执行都是近似间隔1秒。
如果前一个任务执行需要2秒,当前一个任务运行结束后,第二个任务会立刻运行。,
(2)scheduleWithFixedDelay
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,//初始延迟时间 long delay,//两个任务的间隔 TimeUnit unit);//单位
假设delay=1,unit为秒,如果前一个任务执行需要2秒,当前一个任务运行结束后,第二个任务会等待1秒才会运行,这也是和scheduleAtFixedRate的差异