java-进程和线程原理及应用

本文深入探讨了Java中的进程和线程概念,详细解释了线程的创建、信息获取、优先级、状态监控以及中断异常处理。重点讨论了线程池的工作原理,包括线程池的状态转换、Tomcat内置线程池的特性,以及如何自定义线程池。同时,文章还涉及了Spring中@Async线程池和@Scheduled线程池的配置,以及Netty和Reactor-Netty的线程池模型。最后,分析了Lettuce连接Redis时的线程池配置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

进程和线程作为操作系统的基本对象,理解它们有助于更好的编写可靠高效的代码。

进程

一个程序在操作系统中执行后,运行起来的单元就是进程。一个程序可以多次执行,运行出多个进程。比如google浏览器,每次执行都会打开一个进程,这个进程为用户提供浏览网页服务。

线程

操作系统最小的执行单元,也是操作系统任务调度的最小单元。

线程的创建

newRunnableExecutorService

线程的信息

线程的信息可以通过java.lang.Thread类获取。

线程的优先级

线程是由操作系统调度的,而操作系统一般是抢占式调度,即会根据线程的优先级决定哪个线程有更大的概率获得CPU时间片。

线程的状态

在这里插入图片描述

可以通过VisualVM查看,参考博客

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定义了线程池参数和线程池关闭的参数。

默认线程池

默认线程池ThreadPoolExecutororg.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration创建,被对象ThreadPoolTaskExecutor持有,可以通过TaskExecutorCustomizer对象配置。

参考org.springframework.scheduling.concurrent.ThreadPoolTaskExecutorinitializeExecutor方法。

自定义线程池

实现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定义了线程池的参数和线程池关闭的参数。

默认线程池

默认线程池ScheduledExecutorServiceorg.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration创建,被对象ThreadPoolTaskScheduler持有,可以通过TaskSchedulerCustomizer对象配置。

参考org.springframework.scheduling.concurrent.ThreadPoolTaskSchedulerinitializeExecutor方法。

自定义线程池

实现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事件循环。
在这里插入图片描述

为什么bossworkerrunning,而handlerpark呢?请读者自己思考。

接着,又发送了一个http请求,可以发现,开启了新的worker事件循环和handler事件循环。
在这里插入图片描述

为什么会开启一个新的事件循环而不是用旧的呢?答案是io.netty.util.concurrent.EventExecutorChooserFactory$EventExecutorChooser

接着,一次发送100个http请求,可以看到所有的事件循环都被开启了,此时服务器进入火力全开状态。
在这里插入图片描述

事件循环组的所有事件循环都开启了之后,就不会开启新的事件循环了。也就是说,最终启动线程数量是所有事件循环组的大小之和。

配置netty事件循环的事件队列大小

为了防止放在事件队列的任务过多而导致服务器OOM,需要配置每个事件循环的事件队列大小。具体配多少,看服务器的配置和需求。

首先来看看默认的大小。
在这里插入图片描述
在这里插入图片描述
可以看到,队列大小由io.netty.eventLoop.maxPendingTasksio.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操作符就是配置事件循环组的,可以配置类似nettybossworker两个事件循环组的大小。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个请求,程序创建了8reactor消息循环组。
在这里插入图片描述

配置自定义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个请求,可以看到有1boss线程和2worker线程。
在这里插入图片描述

配置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的配置方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值