线程池简述及使用
线程
定义
线程,程序执行流的最小执行单元,是进程中的实际运作单位,经常容易和进程这个概念混淆。那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程
线程生命周期
线程池
定义
在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在java中,内存资源是极其宝贵的,所以,就有了线程池的概念。
线程池:java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
那么,我们应该如何创建一个线程池?java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
执行流程
JDK源码(精简)
public void execute(Runnable command){
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(c) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false)
}
else if (!addWorker(command, false))
reject(command);
}
RejectedExecutionHandler(拒绝策略)
当队列和线程池都满了的时候,再有新的任务到达,就必须要有一种方法来处理新的任务。Java线程池中提供了以下四种策略:
- AbortPolicy:直接抛出异常(默认)
- CallerRunsPolicy:让调用者帮助跑这个任务
- DiscardOldestPolicy:丢弃队列里最老的那个任务,执行当前任务
- DiscardPolicy:不处理,直接扔掉
例如,我们看一个例子
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
这个例子就是,如果线程池和队列都满了,那就作为调用者,你就直接自己去执行r.run吧,我线程池是不管了。另外,让调用者去自己执行,也可以让调用者不要再往线程池里放任务了,有帮于减轻线程池压力。
引入问题:AbortPolicy、DiscardOldestPolicy、DiscardPolicy都是会造成任务的丢失,那CallerRunsPolicy会造成任务的丢失吗?如果线程池和队列都满了,新进来的一个任务也让调用者自己执行了,后续的任务会被如何处理?或者问:线程池丢弃策略为CallerRunsPolicy时,来了一个任务,发现当前运行的线程已经等于最大的线程数,caller线程在忙怎么办?
答案:CallerRunsPolicy这个策略不会造成任务的丢失,当线程池和队列都满了情况下,新进来的一个任务直接调用线程的run()来执行,当后续仍然有任务进来,会进行阻塞,当线程由空闲线程或调用run()的线程执行完毕,被阻塞的任务才能执行
线程池的invokeAll、submit、execute方法的区别
submit()和execute()的区别
JDK5以后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类,两者都可以被ExecutorService执行,他们的区别是:
execute(Runnable x)没有返回值,可以执行任务,但无法判断任务是否成功完成,会将异常抛出——实现Runnable接口
submit(Runnable x)返回一个future对象,可以用这个future来判断任务是否成功完成,会将异常吃掉,如果使用submit要么有get()拉取异常处理,要么自己写try catch把任务执行的逻辑包起来——实现Callable接口
invokeAll和invokeAny的区别
invokeAll触发执行任务列表,返回的结果顺序也与任务在任务列表中的顺序一致,所有线程执行完成任务后才返回结果,如果设置了超时时间,未超时完成则正常返回结果,如果超时未完成则报异常
invokeAny将第一个得到的结果作为返回值,然后立刻终止所有的线程。如果设置了超时时间,未超时完成则正常返回,如果超时未完成则报超时异常
总结:
- invokeAll和invokeAny会直接造成主线程阻塞(需要设置超时时间),等待所有任务执行完成后返回结果,主线程才能继续执行
- submit不会造成主线程阻塞,在后面执行get()方法的时候阻塞,超时时间在get里面设置
- execute会新开启线程执行执行任务,不会阻塞主线程,但无返回结果
submit()吃掉“异常”
submit提交的任务会被包装成FutureTask实例,异常在内部被“吃掉”了。从源码可以看到异常被set出来了,可以用get主动拉取到
public class FutureTask<V> implements RunnableFuture<V> {
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
}
submit()操作一般用于直接进行方法操作,如submit(()->{doSomeThing;}),当然也可以用submit(new Callable的实现类)
execute()操作一般用于无需返回结果的操作,如发送消息,不会造成阻塞,用于异步
模拟示例
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest {
private static ExecutorService threadPool = new ThreadPoolExecutor(
5,
10,
5L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder().setNameFormat("test-thread-%d").build()
);
public static void main(String[] args) throws InterruptedException {
System.out.println("批量任务执行开始时间:"+System.currentTimeMillis());
List<DemoThread> callableList = Lists.newArrayList();
for (int i = 0; i < 40; i++) {
DemoThread demoThread = new DemoThread();
demoThread.setOrderNo(i);
demoThread.setQueueWaitTime(System.currentTimeMillis());
// Future<String> future = threadPool.submit(demoThread);
// 这里调用future.get()和不调用完全是两种情况,调用get()会造成主线程阻塞
// System.out.println(future.get());
callableList.add(demoThread);
}
// 这里可以看invokeAll时,拒绝策略造成的影响,还有不设置超时时间的影响
List<Future<String>> futureList = threadPool.invokeAll(callableList, 40, TimeUnit.SECONDS);
System.out.println("批量任务执行结束时间:"+System.currentTimeMillis());
threadPool.shutdown();
}
static class DemoThread implements Callable<String> {
private int orderNo;
private Long queueWaitTime;
private Long startExeTime;
private Long endExeTime;
public void setOrderNo(int orderNo) {
this.orderNo = orderNo;
}
public void setQueueWaitTime(Long queueWaitTime) {
this.queueWaitTime = queueWaitTime;
}
@Override
public Object call() throws Exception {
// 休眠1~3秒
long sleepMillis = 1000L + new Random().nextInt(2000);
startExeTime = System.currentTimeMillis();
Thread.sleep(sleepMillis);
endExeTime = System.currentTimeMillis();
System.out.println("线程名称为:" + String.format("%-16s", Thread.currentThread().getName())
+ "线程序号为:" + String.format("%-10s", orderNo)
+ "休眠毫秒数为:" + String.format("%-10s", sleepMillis)
+ "等待毫秒数为:" + String.format("%-10s", TimeUnit.MILLISECONDS.toMillis(startExeTime - queueWaitTime))
+ "执行毫秒数为:" + String.format("%-10s", TimeUnit.MILLISECONDS.toMillis(endExeTime - startExeTime))
);
return "线程序号为" + orderNo;
}
}
}
线程池实际使用
批量查询订单详情
public class ThreadPoolTest {
@Resource
private UpdateOrderRpc updateOrderRpc;
private static ExecutorService orderQueryExecutor = new ThreadPoolExecutor(
5,
10,
5L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder().setNameFormat("test-thread-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public List<OrderDetail> queryOrderDetailList(List<OrderInfo> infoList) {
List<QueryOrderThread> threadList = new Array<>(infoList.size());
for(OrderInfo orderInfo : infoList) {
QueryOrderThread thread = new QueryOrderThread();
thread.setQueryOrderRpc(queryOrderRpc);
thread.setOrderInfo(orderInfo);
threadList.add(thread);
}
List<OrderDetail> detailList = new ArrayList();
try {
List<Future<OrderDetail>> futureList = orderQueryExecutor.invokeAll(threadList);
for(Future<UpdateOrderResult> future: futureList){
try {
if (future.get() != null){
resultList.add(future.get());
}
} catch(Exception e){
log.error(xxx);
}
}
} catch(Exception e){
log.error(xxx);
}
return detailList;
}
}
引入问题:这个线程池创建为什么要用CallerRunsPolicy的拒绝策略?如果改成其它类型的拒绝策略会有什么问题?invokeAll在使用上有什么注意点?并且这里的队列使用了SynchronousQueue是为什么?
使用CallerRunsPolicy一是为了防止任务丢失,二是使用其他拒绝策略造成任务丢失,invokeAll在无法拿到所有的future时会一直处于阻塞的状态,如果不希望一直等待,则使用invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit),这里由于是查询操作,所以对于队列也并非只能使用SynchronousQueue,也可以使用其他队列,但是队列的长度不易过长,防止任务在队列中等待超时,从而造成获取结果失败,队列可以new LinkedBlockingDeque(1)
批量提交订单状态
public class ThreadPoolTest {
@Resource
private UpdateOrderRpc updateOrderRpc;
private static ExecutorService orderUpdateExecutor = new ThreadPoolExecutor(
5,
10,
5L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder().setNameFormat("test-thread-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public List<UpdateOrderResult> updateOrderList(List<OrderInfo> infoList) {
List<UpdateOrderThread> threadList = new Array<>();
for(OrderInfo orderInfo : infoList) {
UpdateOrderThread thread = new UpdateOrderThread();
thread.setUpdateOrderRpc(updateOrderRpc);
thread.setOrderInfo(orderInfo);
threadList.add(thread);
}
List<UpdateOrderResult> resultList = new ArrayList();
try {
List<Future<UpdateOrderResult>> futureList = orderUpdateExecutor.invokeAll(threadList);
for(Future<UpdateOrderResult> future: futureList){
try {
if (future.get() != null){
resultList.add(future.get());
}
} catch(Exception e){
log.error(xxx);
}
}
} catch(Exception e){
log.error(xxx);
}
return resultList;
}
引入问题:这个线程池队列使用了SynchronousQueue是为什么?
使用SynchronousQueue是为了防止宕机时队列中未执行的任务丢失,如果设置的队列有长度或者无界队列,由于队列中的任务处于内存中,在重启或者宕机时,队列中的任务就会丢失掉,从而造成丢弃掉的任务得不到执行,造成结果失败或者难以定位原因
注意:特别对于使用多线程发送MQ来更新数据状态的任务,一定不要使用无界队列或有长度的队列,因为一旦宕机,队列中的任务将会丢失,从而造成这些任务未发送出MQ(因为一般正确的数据操作是先修改本地数据状态再发送MQ更新远程数据)
ElasticSearch中用于快速检索的类
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.0.0</version>
</dependency>
可以先利用最大线程,再放置队列中
public class EsExecutors {
public static EsThreadPoolExecutor newScaling(String name, int min, int max, long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory, ThreadContext contextHolder) {
ExecutorScalingQueue<Runnable> queue = new ExecutorScalingQueue<>();
EsThreadPoolExecutor executor = new EsThreadPoolExecutor(name, min, max, keepAliveTime, unit, queue, threadFactory, new ForceQueuePolicy(), contextHolder);
queue.executor = executor;
return executor;
}
static class ExecutorScalingQueue<E> extends LinkedTransferQueue<E> {
ThreadPoolExecutor executor;
ExecutorScalingQueue() {
}
@Override
public boolean offer(E e) {
// first try to transfer to a waiting worker thread
if (!tryTransfer(e)) {
// check if there might be spare capacity in the thread
// pool executor
int left = executor.getMaximumPoolSize() - executor.getCorePoolSize();
if (left > 0) {
// reject queuing the task to force the thread pool
// executor to add a worker if it can; combined
// with ForceQueuePolicy, this causes the thread
// pool to always scale up to max pool size and we
// only queue when there is no spare capacity
return false;
} else {
return super.offer(e);
}
} else {
return true;
}
}
}
/**
* A handler for rejected tasks that adds the specified element to this queue,
* waiting if necessary for space to become available.
*/
static class ForceQueuePolicy implements XRejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// force queue policy should only be used with a scaling queue
assert executor.getQueue() instanceof ExecutorScalingQueue;
executor.getQueue().put(r);
} catch (final InterruptedException e) {
// a scaling queue never blocks so a put to it can never be interrupted
throw new AssertionError(e);
}
}
@Override
public long rejected() {
return 0;
}
}
}
该类的使用可以在org.elasticsearch.discovery.SeedHostsResolver找到