【Java 线程池】线程池的源码应用
1 前言
我们平时异步会用到线程池,那你怎么用的呢?用的对不对呢?这节我们看看源码中的一些应用场景,看看他们是如何设置参数,如何停止的。
回忆一下线程池的几个参数:核心线程数、最大线程数、空闲时间、队列、线程工厂、拒绝策略。
2 源码应用
我们就看看他们是如何创建线程池、线程池的参数设置、线程的停止等几个角度看起。
2.1 Tomcat 里的 EndPoint 类
我们平时 SpringBoot 会配置如下参数,他们其实就是设置线程池的。
# 设置最小线程数
server.tomcat.min-spare-threads=50
# 设置最大线程数
server.tomcat.max-threads=200
2.1.1 线程池的创建以及参数设置
直接看 AbstractEndpoint:
public void createExecutor() { internalExecutor = true; // 哇,虚拟线程,这个没用过,我们这里不看 if (getUseVirtualThreads()) { executor = new VirtualThreadExecutor(getName() + "-virt-"); } else { // 参数队列 TaskQueue taskqueue = new TaskQueue(); // 线程工厂 TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); // 创建线程 空闲时间60s 拒绝策略采取默认拒绝抛异常的策略 executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf); taskqueue.setParent( (ThreadPoolExecutor) executor); } } // 线程池的核心线程数,取最大最小的小者 public int getMinSpareThreads() { return Math.min(getMinSpareThreadsInternal(), getMaxThreads()); } // 最大线程数 public int getMaxThreads() { if (internalExecutor) { return maxThreads; } else { return -1; } }
可以看到:核心线程数取的 minSpareThreads 和 maxThreads 的最小者,最大线程数取的 maxThreads 默认200,队列用的是 TaskQueue,线程工厂用的是 TaskThreadFactory,空闲时间60s,拒绝策略用的默认的抛异常的那个策略。
那我们看看它的队列:TaskQueue taskqueue = new TaskQueue();
public class TaskQueue extends LinkedBlockingQueue<Runnable> { private static final long serialVersionUID = 1L; protected static final StringManager sm = StringManager.getManager(TaskQueue.class); private transient volatile ThreadPoolExecutor parent = null; public TaskQueue() { super(); } }
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { private static final long serialVersionUID = -6903933977591709194L; public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } ... }
可以看到它用的实际上是:LinkedBlockingQueue,队列大小默认 Integer 的最大值哈。
那我们这里顺便说个别的,我们会设置等待队列的大小,怎么没看到是设置队列的大小呢?
# 设置等待队列的最大容量为200
server.tomcat.max-connections=200
其实人家把这个前置了,交给了 LimitLatch ,内部有个 AQS 来控制的哈。
public void setMaxConnections(int maxCon) { this.maxConnections = maxCon; LimitLatch latch = this.connectionLimitLatch; if (latch != null) { // Update the latch that enforces this if (maxCon == -1) { releaseConnectionLatch(); } else { latch.setLimit(maxCon); } } else if (maxCon > 0) { initializeConnectionLatch(); } }
我们继续看下它的线程工厂:TaskThreadFactory
public class TaskThreadFactory implements ThreadFactory { private final ThreadGroup group; // 内部编号器 private final AtomicInteger threadNumber = new AtomicInteger(1); // 线程名称前缀-业务相关 private final String namePrefix; // 是否守护线程 private final boolean daemon; // 调度优先级 private final int threadPriority; public TaskThreadFactory(String namePrefix, boolean daemon, int priority) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); this.namePrefix = namePrefix; this.daemon = daemon; this.threadPriority = priority; } @Override public Thread newThread(Runnable r) { TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement()); t.setDaemon(daemon); t.setPriority(threadPriority); if (Constants.IS_SECURITY_ENABLED) { // Set the context class loader of newly created threads to be the // class loader that loaded this factory. This avoids retaining // references to web application class loaders and similar. PrivilegedAction<Void> pa = new PrivilegedSetTccl( t, getClass().getClassLoader()); AccessController.doPrivileged(pa); // This method may be triggered from an InnocuousThread. Ensure that // the thread inherits an appropriate AccessControlContext pa = new PrivilegedSetAccessControlContext(t); AccessController.doPrivileged(pa); } else { t.setContextClassLoader(getClass().getClassLoader()); } return t; } }
线程又用 TaskThread 包装了一层,内部主要多加了一个创建时间。
public class TaskThread extends Thread { private static final Log log = LogFactory.getLog(TaskThread.class); private final long creationTime; public TaskThread(ThreadGroup group, Runnable target, String name) { super(group, new WrappingRunnable(target), name); this.creationTime = System.currentTimeMillis(); } public TaskThread(ThreadGroup group, Runnable target, String name, long stackSize) { super(group, new WrappingRunnable(target), name, stackSize); this.creationTime = System.currentTimeMillis(); } ... }
2.1.2 线程池的停止
看完创建,我们继续看看停止:
// 线程池结束的等待时间 private long executorTerminationTimeoutMillis = 5000; public long getExecutorTerminationTimeoutMillis() { return executorTerminationTimeoutMillis; } public void shutdownExecutor() { Executor executor = this.executor; if (executor != null && internalExecutor) { this.executor = null; if (executor instanceof ThreadPoolExecutor) { //this is our internal one, so we need to shut it down ThreadPoolExecutor tpe = (ThreadPoolExecutor) executor; // 直接调用 shutdownNow() 不再接收新的 正在运行的和在队列呆着的都中断掉 tpe.shutdownNow(); long timeout = getExecutorTerminationTimeoutMillis(); if (timeout > 0) { try { // 等待线程池结束 默认5s tpe.awaitTermination(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // Ignore } // 打印 if (tpe.isTerminating()) { getLog().warn(sm.getString("endpoint.warn.executorShutdown", getName())); } } // 释放引用 TaskQueue queue = (TaskQueue) tpe.getQueue(); queue.setParent(null); } } }
可以看到,直接调用线程池的 shutdownNow()哈,并且默认会等待 5s哈。
2.2 Apollo 里的 ReleaseMessageScanner
Apollo 当你在门户发布一条配置变更时,那下游的客户端怎么知道的呢?其实是 Service 服务有个调度的线程池每隔 1s 去捞一下数据库里的变更信息(类似基于数据库的生产消费),发现有新的配置变更后,便通知下游的客户端,去获取新的配置的。
2.2.1 线程池的创建以及参数设置
public class ReleaseMessageScanner implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageScanner.class); ... private long maxIdScanned; public ReleaseMessageScanner() { listeners = Lists.newCopyOnWriteArrayList(); // 创建调度线程池 核心线程数=1 线程工厂 ApolloThreadFactory executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory .create("ReleaseMessageScanner", true)); missingReleaseMessages = Maps.newHashMap(); } } // Executors 类 public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); } // ScheduledThreadPoolExecutor 类 // 最大线程数 Integer最大值,空闲时间0 队列用的延时队列 public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory); }
我们继续看看调度任务:
// 默认扫描间隔时间 1000ms 也就是 1s private static final int DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS = 1000; public int releaseMessageScanIntervalInMilli() { int interval = getIntProperty("apollo.message-scan.interval", DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS); return checkInt(interval, 100, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS); } @Override public void afterPropertiesSet() throws Exception { databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli(); maxIdScanned = loadLargestMessageId(); // 固定时间间隔的方式运行 这里顺便带一嘴和频率的差异 比如每隔5s跑一次 当实际执行时间小于5s的话,那它俩效果一样 等到下一个5s运行,当实际运行时间大于5s,当实际运行完会继续执行,而固定间隔的会再等5s才运行 executorService.scheduleWithFixedDelay(() -> { Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage"); try { scanMissingMessages(); scanMessages(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Scan and send message failed", ex); } finally { transaction.complete(); } }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS); }
我们再看看它的线程工厂:
public class ApolloThreadFactory implements ThreadFactory { private static Logger log = LoggerFactory.getLogger(ApolloThreadFactory.class); // 计数器 private final AtomicLong threadNumber = new AtomicLong(1); // 名称前缀 private final String namePrefix; // 是否守护 private final boolean daemon; // 线程组 private static final ThreadGroup threadGroup = new ThreadGroup("Apollo"); public static ThreadGroup getThreadGroup() { return threadGroup; } public static ThreadFactory create(String namePrefix, boolean daemon) { return new ApolloThreadFactory(namePrefix, daemon); } public Thread newThread(Runnable runnable) { Thread thread = new Thread(threadGroup, runnable,// threadGroup.getName() + "-" + namePrefix + "-" + threadNumber.getAndIncrement()); thread.setDaemon(daemon); if (thread.getPriority() != Thread.NORM_PRIORITY) { thread.setPriority(Thread.NORM_PRIORITY); } return thread; } }
可以看到跟 Tomcat 的线程工厂差不多,计数器、名称前缀,嘿嘿我们是不是能直接拿过来抄。
2.2.2 线程池的停止
咦,没看到有停止,服务停了就停了哈哈
3 小结
好啦,暂时看到这里哈,有理解不对的地方欢迎指正哈。