前言
进程和线程作为操作系统的基本对象,理解它们有助于更好的编写可靠高效的代码。
进程
一个程序在操作系统中执行后,运行起来的单元就是进程。一个程序可以多次执行,运行出多个进程。比如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的配置方法。
本文深入探讨了Java中的进程和线程概念,详细解释了线程的创建、信息获取、优先级、状态监控以及中断异常处理。重点讨论了线程池的工作原理,包括线程池的状态转换、Tomcat内置线程池的特性,以及如何自定义线程池。同时,文章还涉及了Spring中@Async线程池和@Scheduled线程池的配置,以及Netty和Reactor-Netty的线程池模型。最后,分析了Lettuce连接Redis时的线程池配置。

4898

被折叠的 条评论
为什么被折叠?



