前言
进程和线程作为操作系统的基本对象,理解它们有助于更好的编写可靠高效的代码。
进程
一个程序在操作系统中执行后,运行起来的单元就是进程。一个程序可以多次执行,运行出多个进程。比如google浏览器,每次执行都会打开一个进程,这个进程为用户提供浏览网页服务。
线程
操作系统最小的执行单元,也是操作系统任务调度的最小单元。
线程的创建
new
,Runnable
,ExecutorService
线程的信息
线程的信息可以通过java.lang.Thread
类获取。
线程的优先级
线程是由操作系统调度的,而操作系统一般是抢占式调度,即会根据线程的优先级决定哪个线程有更大的概率获得CPU时间片。
线程的状态
InterruptedException
线程收到中断异常后,要正确处理线程已经中断这个事实。传播这个异常或者调用Thread.currentThread.interrupt()
来设置中断状态,让依赖线程中断状态的代码能够正确运行。
线程池
线程池的运作原理
线程池的状态转换
不用Executors
的静态方法(要么队列无限,容易内存oom,要么线程数无限,容易cpu爆满)创建线程池,而需根据实际业务用构造方法创建。
tomcat线程池
Acceptor线程
名称中包含-Acceptor-
,默认只有一个。功能是死循环阻塞接收连接,然后注册到poller(AbstractEndpoint.setSocketOptions(**)
)。
Poller线程
名称中包含-ClientPoller-
,默认个数Math.min(2,Runtime.getRuntime().availableProcessors())
。不断轮询Selector
,把IO事件派发给线程池执行(AbstractEndpoint.processSocket(**)
)。
配置类
默认线程池的参数可以由org.springframework.boot.autoconfigure.web.ServerProperties
配置类指定。
内置线程池
tomcat默认使用内置线程池。名称包含-exec-
。
org.apache.tomcat.util.net.AbstractEndpoint.java
- 核心线程个数:min-spare
- 最大线程个数:max
内置线程池的TaskQueue
重载了offer
方法,里面加入了一个有趣的逻辑,当线程池线程数量小于最大线程数量时,直接返回false
,如下图。
这个改变直接影响了线程池execute
方法的行为。当核心线程满了之后,任务不会直接入队,而是会进入第三个判断,创建一个非核心线程执行;这点和一般的线程池的行为不同。
当然,这里的任务队列的大小也是无限的,这意味着,任务足够多的时候,也是会撑爆内存而OOM
。
Integer.MAX_VALUE的值。如果达到最大值,内存根本hold不住。
另外,如果核心线程数为0,线程池也会创建一个线程执行任务(execute
调用addWorker(null, false)
)。但是只会创建一个线程,所有任务在一个线程执行。
spring自定义tomcat线程池
@Component
public class TomcatConfig implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered {
@Resource
private ServerProperties serverProperties;
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(ConfigurableTomcatWebServerFactory factory) {
if (factory instanceof TomcatServletWebServerFactory) {
//貌似是传说中的异步非阻塞模型。
((TomcatServletWebServerFactory) factory).setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
}
factory.addConnectorCustomizers((connector) -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol) {
AbstractProtocol<?> protocol = (AbstractProtocol<?>) handler;
protocol.setExecutor(getExecutor());
}
});
}
private Executor getExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
serverProperties.getTomcat().getThreads().getMinSpare(),
serverProperties.getTomcat().getThreads().getMax(),
60, TimeUnit.SECONDS,
new TaskQueue(serverProperties.getTomcat().getThreads().getMax()),
new DefaultThreadFactory("civic-tomcat")
);
//核心线程也被回收
executor.allowCoreThreadTimeOut(true);
return executor;
}
}
allowCoreThreadTimeOut(true)
配置可回收核心线程,当请求洪峰来了之后,会创建大量线程,当洪峰走了之后这些线程可以被回收(如下图keepAliveTime
之后的骤降)。既减少了CPU压力,也减小了堆压力。
当然,也可以不回收核心线程,避免线程创建和销毁带来的性能开销。
@Async线程池
配置类
org.springframework.boot.autoconfigure.task.TaskExecutionProperties
定义了线程池参数和线程池关闭的参数。
默认线程池
默认线程池ThreadPoolExecutor
由org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
创建,被对象ThreadPoolTaskExecutor
持有,可以通过TaskExecutorCustomizer
对象配置。
参考
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
的initializeExecutor
方法。
自定义线程池
实现org.springframework.scheduling.annotation.AsyncConfigurer
类。
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncUncaughtExceptionHandler();
}
@Scheduled线程池
配置类
org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
定义了线程池的参数和线程池关闭的参数。
默认线程池
默认线程池ScheduledExecutorService
由org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
创建,被对象ThreadPoolTaskScheduler
持有,可以通过TaskSchedulerCustomizer
对象配置。
参考
org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
的initializeExecutor
方法。
自定义线程池
实现org.springframework.scheduling.annotation.SchedulingConfigurer
类。
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
Netty线程池
netty的线程池概念和jdk的线程池概念不太一样。netty的线程池其实是一个事件循环组(MultithreadEventLoopGroup)
,每个线程实际上是一个事件循环(SingleThreadEventLoop)
,每个事件循环的线程启动之后,会开启一个死循环不断拿事件队列的事件去消费。jdk中的线程池只有一个任务队列,被线程池的所有线程共享,队列的任务被哪个线程处理是不确定的(可能是池中创建好的线程,也可能是创建新的线程去处理)。而netty每个线程都有一个事件队列,并且维护了一个死循环去处理这些事件,所以事件进入了哪个事件循环,就一定会被那个线程处理。
配置netty事件循环组
netty的事件循环组根据功能可以分为三类:MainReactor(监听新连接事件)、SubReactor(监听读写事件)、Hander(编解码和业务逻辑)。
具体可以参考Reactor。
一个完整的简单http服务器如下:
EventLoopGroup boss = new NioEventLoopGroup(1, new DefaultThreadFactory("boss"));
EventLoopGroup worker = new NioEventLoopGroup(new DefaultThreadFactory("worker"));
EventLoopGroup handler = new DefaultEventLoopGroup(new DefaultThreadFactory("handler"));
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(5555);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.handler(new LoggingHandler(LogLevel.DEBUG));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(handler,
new HttpServerCodec(),
new HttpObjectAggregator(512 * 1024),
new HttpRequestHandler());
}
});
ChannelFuture bindFuture = serverBootstrap.bind().sync();
log.info("服务已绑定");
ChannelFuture closeFuture = bindFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e) {
log.info("服务启动异常", e);
} finally {
handler.shutdownGracefully();
worker.shutdownGracefully();
boss.shutdownGracefully();
}
其中,boss
是处理新连接的事件循环组,只处理SelectionKey.OP_ACCEPT
事件;worker
是处理读写io事件的事件循环组,只处理SelectionKey.OP_READ
事件;handler
是编解码和业务逻辑的事件循环组。
事件循环在接受第一个事件时就开启了,一般是注册通道
事件,事件循环一旦开启,不会自动关闭(这里和线程池的非核心线程keepAliveTime
到期后会自动回收不太一样)。这里boss
只配置了一个事件循环,worker
使用默认值,也就是核心数的两倍,hander
也是一样。
程序启动时,注册了NioServerSocketChannel
通道,启动了boss
唯一的一个事件循环所在的线程。
使用jmeter发送一个http请求。可以看到服务器启动了一个worker
事件循环和一个handler
事件循环。
为什么
boss
和worker
是running
,而handler
是park
呢?请读者自己思考。
接着,又发送了一个http请求,可以发现,开启了新的worker
事件循环和handler
事件循环。
为什么会开启一个新的事件循环而不是用旧的呢?答案是
io.netty.util.concurrent.EventExecutorChooserFactory$EventExecutorChooser
。
接着,一次发送100个http请求,可以看到所有的事件循环都被开启了,此时服务器进入火力全开状态。
事件循环组的所有事件循环都开启了之后,就不会开启新的事件循环了。也就是说,最终启动线程数量是所有事件循环组的大小之和。
配置netty事件循环的事件队列大小
为了防止放在事件队列的任务过多而导致服务器OOM
,需要配置每个事件循环的事件队列大小。具体配多少,看服务器的配置和需求。
首先来看看默认的大小。
可以看到,队列大小由io.netty.eventLoop.maxPendingTasks
和io.netty.eventexecutor.maxPendingTasks
参数决定,默认是Integer.MAX_VALUE
。
启动时设置这两个参数即可。
-Dio.netty.eventLoop.maxPendingTasks=1024
-Dio.netty.eventexecutor.maxPendingTasks=1024
Reactor-Netty线程池
Reactor-Netty是基于netty写的,所以线程池模型和netty相似,只不过reactor-netty又加了一个接口reactor.netty.resources.LoopResources
来表示线程池资源,默认实现是reactor.netty.resources.DefaultLoopResources
。
配置reactor-netty事件循环组
一个简单的reactory-netty的http服务器配置如下:
DisposableServer disposableServer = HttpServer.create()
.port(5556)
.runOn(LoopResources.create("civic", 1, 2, false))
.accessLog(true)
.handle((req, res) -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return res.sendString(Flux.just("hello"));
})
.bind()
.block();
log.info("服务绑定成功");
assert disposableServer != null;
disposableServer.onDispose().block();
其中的runOn
操作符就是配置事件循环组的,可以配置类似netty
的boss
和worker
两个事件循环组的大小。LoopResources.create("civic", 1, 2, false)
第一个参数指定线程的名称,第二个参数指定boss
大小,第三个参数指定worker
大小,第四个参数指定是否守护线程。
可惜的是,
reactor-netty
不能配置handler
事件循环组,reactor.netty.transport.TransportConfig$TransportChannelInitializer
并未提供这样的参数。
启动程序,使用jmeter
发10个请求,可以看到程序启动了两个worker
事件循环组。
从图中可以看出,worker
线程进行了睡眠,所以QPS
只接近2
。
如果把睡眠时间缩短为500ms
,同样发送10个请求。可以看到QPS
接近4
。
缩短接口响应时间是提高QPS的重要手段之一。
配置reactor-netty事件循环的事件队列大小
类比netty
的配置方法。
Spring Webflux线程池
spring-webflux是基于reactor-netty
的一套web服务框架。它的线程池通过org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryConfiguration
类配置的ReactorResourceFactory
决定。
默认boss
大小是-1
(表示和worker
共用同一个消息循环组),worker
大小是cpu核心数。使用jmeter
发送16
个请求,程序创建了8
个reactor
消息循环组。
配置自定义Spring Webflux线程池
要自定义webflux线程池,只需要自定义ReactorResourceFactory
替换默认的即可。
@Configuration
@Slf4j
public class LoopConfig {
@Bean
ReactorResourceFactory reactorResourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
factory.setLoopResources(LoopResources.create("civic", 1, 2, false));
return factory;
}
}
使用jmeter
发送16个请求,可以看到有1
个boss
线程和2
个worker
线程。
配置webflux事件循环的事件队列大小
类比netty
的配置方法。
lettuce线程池
启动程序后,查看线程使用情况如下:
lettuce使用netty客户端api连接redis服务,配置lettuce线程池,其实是配置netty客户端线程池。
配置线程池
默认客户端线程池大小是由下面代码决定的:
如果可用核心数大于2,则是可用核心数;如果可用核心数小于2,则是2。
修改默认线程池数量大小的大小如下:
DefaultClientResources clientResources = DefaultClientResources.builder().ioThreadPoolSize(8).computationThreadPoolSize(8).build();
ioThreadPoolSize
决定lettuce-eventExecutorLoop
大小,computationThreadPoolSize
决定lettuce-nioEventLoop
大小。
配置事件循环的事件队列大小
类比netty
的配置方法。