RocketMQ 5.1.0 源码详解 | Producer 启动流程

news/2025/4/1 20:01:37/

文章目录

  • 初始化DefaultMQProducer实例
  • 启动流程
    • DefaultMQProducer#start
    • DefaultMQProducerImpl#start
    • MQClientInstance#start
    • 启动流程总结
  • 实例内容

初始化DefaultMQProducer实例

初始化一个 DefaultMQProducer 对象的代码如下

// 返回一个producer对象
DefaultMQProducer producer = new DefaultMQProducer();
// 设置组名
producer.setProducerGroup("ProducerGroupName");
// 设置NameServer地址
producer.setNamesrvAddr("127.0.0.1:9876");

在初始化 DefaultMQProducer 时会初始化一个 DefaultMQProducerImpl 实例并赋值给 producer 的成员变量

public DefaultMQProducer() {this(null, MixAll.DEFAULT_PRODUCER_GROUP, null);
}public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) {this.namespace = namespace;this.producerGroup = producerGroup;// 将defaultMQProducerImpl对象保存在成员变量中defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}

同时,在初始化 DefaultMQProducerImpl 实例时也会将 producer 对象作为成员变量保存在 DefaultMQProducerImpl 实例中

构造 defaultMQProducerImpl 的代码如下

public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {// 将defaultMQProducer对象保存在成员变量中this.defaultMQProducer = defaultMQProducer;this.rpcHook = rpcHook;this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(),1000 * 60,TimeUnit.MILLISECONDS,this.asyncSenderThreadPoolQueue,new ThreadFactory() {private AtomicInteger threadIndex = new AtomicInteger(0);@Overridepublic Thread newThread(Runnable r) {return new Thread(r, "AsyncSenderExecutor_" + this.threadIndex.incrementAndGet());}});if (defaultMQProducer.getBackPressureForAsyncSendNum() > 10) {semaphoreAsyncSendNum = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(),10), true);} else {semaphoreAsyncSendNum = new Semaphore(10, true);log.info("semaphoreAsyncSendNum can not be smaller than 10.");}if (defaultMQProducer.getBackPressureForAsyncSendNum() > 1024 * 1024) {semaphoreAsyncSendSize = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(),1024 * 1024), true);} else {semaphoreAsyncSendSize = new Semaphore(1024 * 1024, true);log.info("semaphoreAsyncSendSize can not be smaller than 1M.");}
}

因此 DefaultMQProducerImpl 对象能够通过其保存的 producer 对象实例获取到 producer 的所有参数

经过上面简单的设置后此时 producer 被赋值的成员变量有

producerGroup = "ProducerGroupName"
namesrvAddr = "127.0.0.1:9876"
namespaceInitialized = false
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook) // 这里rpcHook为null

启动流程

DefaultMQProducer#start

只需要执行下面代码即可启动上方初始化的 produer 对象

producer.start();

producerstart() 方法具体的内容如下

public void start() throws MQClientException {// 由于DefaultMQProducer继承了ClientConfig,所以可以直接使用ClientConfig的withNamespace方法this.setProducerGroup(withNamespace(this.producerGroup));// 调用DefaultMQProducerImpl的start方法this.defaultMQProducerImpl.start();// 如果traceDispatcher不为空,则调用traceDispatcher的start方法if (null != traceDispatcher) {try {traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());} catch (MQClientException e) {logger.warn("trace dispatcher start failed ", e);}}
}

可以看到 DefaultMQProducer 只是一个门面类,具体的实现都是由DefaultMQProducerImpl 去做的


由于 Namespace 的存在,因此在启动 producer 时首先会重新设置 producerGroup,我们需要重点关注经过 withNamespace() 方法处理后返回的生产者组名

public String withNamespace(String resource) {// this.getNamespace()不设置的话返回的是nullreturn NamespaceUtil.wrapNamespace(this.getNamespace(), resource);
}

可见 withNamespace() 方法仅仅是调用了 wrapNamespace() 方法,并将 NamespaceproducerGroup 作为参数一并传入

public static String wrapNamespace(String namespace, String resourceWithOutNamespace) {// 如果namespace为空或者resourceWithOutNamespace为空,则直接返回resourceWithOutNamespaceif (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceWithOutNamespace)) {return resourceWithOutNamespace;}// 如果resourceWithOutNamespace是SystemResource,// 或者resourceWithOutNamespace已经组合了Namespace,则直接返回resourceWithOutNamespaceif (isSystemResource(resourceWithOutNamespace) || isAlreadyWithNamespace(resourceWithOutNamespace, namespace)) {return resourceWithOutNamespace;}String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resourceWithOutNamespace);StringBuilder stringBuilder = new StringBuilder();if (isRetryTopic(resourceWithOutNamespace)) {stringBuilder.append(MixAll.RETRY_GROUP_TOPIC_PREFIX);}if (isDLQTopic(resourceWithOutNamespace)) {stringBuilder.append(MixAll.DLQ_GROUP_TOPIC_PREFIX);}// 返回 [RETRY_PREFIX] + [DLQ_PREFIX] + namespace + % + resourceWithoutRetryAndDLQreturn stringBuilder.append(namespace).append(NAMESPACE_SEPARATOR).append(resourceWithoutRetryAndDLQ).toString();}

由于我们并没有设置 producerNamespace,因此会直接返回 producerGroup。造成的效果就是在这个生产者启动过程中第一行代码没有任何效果


traceDispatcher 又是个什么东西?traceDispatcher 的作用是追踪消息的发送和消费的轨迹,它是一个 AsyncTraceDispatcher 对象,它实现了 TraceDispatcher 接口,用于异步地发送追踪消息到 Broker。它可以帮助用户查看每条消息的完整链路数据,包括发送时间、消费时间、存储时间等。我们可以通过使用下面的构造函数构造出一个含有 traceDispatcher 的 DefaultMQProducer 实例

public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook,boolean enableMsgTrace, final String customizedTraceTopic) {this.namespace = namespace;this.producerGroup = producerGroup;defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);//if client open the message trace featureif (enableMsgTrace) {try {AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook);dispatcher.setHostProducer(this.defaultMQProducerImpl);traceDispatcher = dispatcher;this.defaultMQProducerImpl.registerSendMessageHook(new SendMessageTraceHookImpl(traceDispatcher));this.defaultMQProducerImpl.registerEndTransactionHook(new EndTransactionTraceHookImpl(traceDispatcher));} catch (Throwable e) {logger.error("system mqtrace hook init failed ,maybe can't send msg trace data");}}
}

由于我们在初始化 DefaultMQProducer 实例时没有生成 traceDispatcher 实例,因此 null != traceDispatcher 返回 FALSE,不调用 traceDispatcher 的 start 方法

DefaultMQProducerImpl#start

接下来调用 defaultMQProducerImplstart() 方法,方法内容如下:

private ServiceState serviceState = ServiceState.CREATE_JUST;public void start() throws MQClientException {this.start(true);
}public void start(final boolean startFactory) throws MQClientException {// serviceState默认为CREATE_JUSTswitch (this.serviceState) {case CREATE_JUST:// 1. 设置serviceState为START_FAILEDthis.serviceState = ServiceState.START_FAILED;// 2. 检查生产者组名是否合法this.checkConfig();// 3. 如果实例名为默认值则将生产者的instanceName设置为 UtilAll.getPid() + "#" + System.nanoTime()if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {this.defaultMQProducer.changeInstanceNameToPID();}// 4. 使用MQClientManager.getInstance()返回一个单例的MQClientManager对象// defaultMQProducer继承了ClientConfig,因此getOrCreateMQClientInstance方法的参数可以是defaultMQProducer// mQClientFactory是MQClientInstance的一个实例,MQClientInstance是MQClientManager的内部类this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);// 5. 注册producer实例:将生产者组名作为key,defaultMQProducerImpl对象作为value保存到MQClientInstance的producerTable中boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);if (!registerOK) {// 如果注册失败则将serviceState重设为CREATE_JUSTthis.serviceState = ServiceState.CREATE_JUST;throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),null);}// 6. 将defaultMQProducer的createTopicKey作为key,TopicPublishInfo作为value,放入到defaultMQProducerImpl的topicPublishInfoTable中// createTopicKey的默认值为TBW102// topicPublishInfoTable的作用是存储topic的路由信息,包括topic的queue数目、brokerName、brokerId等this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());// 7. 启动MQClientInstance实例if (startFactory) {// MQClientInstance的start方法会启动MQClientInstance的定时任务// 包括定时向所有broker发送心跳、定时清理过期的topic、定时清理过期的consumer、定时清理过期的producermQClientFactory.start();}log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),this.defaultMQProducer.isSendMessageWithVIPChannel());// 8. 如果启动成功则将serviceState设置为RUNNINGthis.serviceState = ServiceState.RUNNING;break;case RUNNING:case START_FAILED:case SHUTDOWN_ALREADY:throw new MQClientException("The producer service state not OK, maybe started once, "+ this.serviceState+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),null);default:break;}// 立即发送心跳到所有brokerthis.mQClientFactory.sendHeartbeatToAllBrokerWithLock();// 开启一个定时任务来处理所有的 Request 状态,对异步的请求根据状态处理回调函数// 这个异步请求指的并不是在 send 中的异步回调机制,而是 Request-Reply 特性,用来模拟 RPC 调用RequestFutureHolder.getInstance().startScheduledTask(this);}

注意上述代码中有一段注释为createTopicKey 的默认值为 TBW102,这个 Topic 在自动创建 topic 时有关键作用

最后的 RequestFutureHolder.getInstance().startScheduledTask(this) 用来扫描和处理过期的异步请求,但是需要注意的是这个异步请求指的并不是在 send 中的异步回调机制,而是 Request-Reply 特性,用来模拟 RPC 调用

RocketMQ 有两种异步请求的方式,一种是在 send 方法中传入一个回调函数,当消息发送成功或失败时,会调用这个回调函数。这种方式不需要等待服务器的响应,只需要等待服务器的确认。

另一种是在 RocketMQ 4.7.0 后加入的 Request-Reply 特性,这种方式是模拟 RPC 调用,需要等待服务器的响应,并返回一个结果。这种方式需要使用 RequestResponseFuture 对象来封装请求和响应的信息。


在使用 getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook) 返回一个 MQClientInstance 对象时,如果 factoryTable 中没有实例的话则会初始化一个新的实例,代码如下:

public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {// 最多情况下 clientId = ip@instanceName@unitName@RequestType.STREAM// 默认情况下 clientId = ip@instanceNameString clientId = clientConfig.buildMQClientId();MQClientInstance instance = this.factoryTable.get(clientId);if (null == instance) {instance =new MQClientInstance(clientConfig.cloneClientConfig(),this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);if (prev != null) {instance = prev;log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);} else {log.info("Created new MQClientInstance for clientId:[{}]", clientId);}}return instance;
}

注意在初始化 MQClientInstance 实例的过程中会初始化一个新的 DefaultMQProducer 实例,与我们一开始就有的 producer 实例不是一个对象

public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {// ...// 此处实例化内部的producer// 用于消费失败或超时的消息,sendMessageBack回发给broker,放到retry topic中重试消费this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP);this.defaultMQProducer.resetClientConfig(clientConfig);// ...
}

此时新初始化的 DefaultMQProducer 实例的 producerGroup = "CLIENT_INNER_PRODUCER"instanceName = "DEFAULT"

接着,在初始化实例后又执行了 this.defaultMQProducer.resetClientConfig(clientConfig) 这行代码

public void resetClientConfig(final ClientConfig cc) {this.namesrvAddr = cc.namesrvAddr;this.clientIP = cc.clientIP;this.instanceName = cc.instanceName;this.clientCallbackExecutorThreads = cc.clientCallbackExecutorThreads;this.pollNameServerInterval = cc.pollNameServerInterval;this.heartbeatBrokerInterval = cc.heartbeatBrokerInterval;this.persistConsumerOffsetInterval = cc.persistConsumerOffsetInterval;this.pullTimeDelayMillsWhenException = cc.pullTimeDelayMillsWhenException;this.unitMode = cc.unitMode;this.unitName = cc.unitName;this.vipChannelEnabled = cc.vipChannelEnabled;this.useTLS = cc.useTLS;this.socksProxyConfig = cc.socksProxyConfig;this.namespace = cc.namespace;this.language = cc.language;this.mqClientApiTimeout = cc.mqClientApiTimeout;this.decodeReadBody = cc.decodeReadBody;this.decodeDecompressBody = cc.decodeDecompressBody;this.enableStreamRequestType = cc.enableStreamRequestType;
}

可以看到,这段代码将 producer 实例的 ClinetConfig 属性完全拷贝了一份给新创建的 DefaultMQProducer 实例的 ClinetConfig 属性。因此这段代码后新初始化的 DefaultMQProducer 实例的 instanceName 不再是默认值而是和 producer 的一致

MQClientInstance#start

接下来我们来看 mQClientFactory.start() 这部分的代码

public void start() throws MQClientException {// 用synchronized修饰保证线程安全性与内存可见性synchronized (this) {switch (this.serviceState) {case CREATE_JUST:this.serviceState = ServiceState.START_FAILED;// If not specified,looking address from name server// 由于传入了NameServer的地址,因此不进入分支if (null == this.clientConfig.getNamesrvAddr()) {this.mQClientAPIImpl.fetchNameServerAddr();}// Start request-response channel// 1. 启动用于和broker通信的netty客户端this.mQClientAPIImpl.start();// Start various schedule tasks// 2. 启动定时任务,包括心跳,拉取topic路由信息,更新broker信息,清理过期消息等this.startScheduledTask();// 3. Start pull servicethis.pullMessageService.start();// Start rebalance service// 4. 启动负载均衡服务(对MQConsumer有效)this.rebalanceService.start();// Start push service// 5. 启动它内部的producer实例// this.defaultMQProducer是在DefaultMQProducerImpl中// 使用MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook)被初始化的this.defaultMQProducer.getDefaultMQProducerImpl().start(false);log.info("the client factory [{}] start OK", this.clientId);this.serviceState = ServiceState.RUNNING;break;case START_FAILED:throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);default:break;}}
}

这段代码的主要任务是启动和broker通信的 Netty 客户端、启动定时任务、启动拉取消息服务、启动负载均衡服务、启动内部生产者实例

其中启动负载均衡服务只是针对消费者而言的,在生产者启动过程中并无作用

mQClientAPIImpl 对象是 RocketMQ 客户端与 Broker 之间通信的实现类

我们先看启动定时任务这个方法做了什么

private void startScheduledTask() {if (null == this.clientConfig.getNamesrvAddr()) {// 如果没有指定namesrv地址,则定时获取namesrv地址this.scheduledExecutorService.scheduleAtFixedRate(() -> {try {MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();} catch (Exception e) {log.error("ScheduledTask fetchNameServerAddr exception", e);}}, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);}// 定期从NameServer更新topic路由信息this.scheduledExecutorService.scheduleAtFixedRate(() -> {try {MQClientInstance.this.updateTopicRouteInfoFromNameServer();} catch (Exception e) {log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);}}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);// 用于定期清除离线Broker,并向所有Broker发送心跳包this.scheduledExecutorService.scheduleAtFixedRate(() -> {try {MQClientInstance.this.cleanOfflineBroker();MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();} catch (Exception e) {log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);}}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);// 定时持久化消费者当前消费进度(对MQConsumer有效)this.scheduledExecutorService.scheduleAtFixedRate(() -> {try {MQClientInstance.this.persistAllConsumerOffset();} catch (Exception e) {log.error("ScheduledTask persistAllConsumerOffset exception", e);}}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);// 根据当前的积压调优线程池的核心线程数this.scheduledExecutorService.scheduleAtFixedRate(() -> {try {MQClientInstance.this.adjustThreadPool();} catch (Exception e) {log.error("ScheduledTask adjustThreadPool exception", e);}}, 1, 1, TimeUnit.MINUTES);
}

其中的定时持久化消费者当前消费进度任务同样只是针对消费者而言的,在生产者启动过程中并无作用

至此,producer 的启动流程结束

启动流程总结

执行 producer.start()启动一个 producer

  1. 重新设置生产者组名
  2. 调用 defaultMQProducerImplstart() 方法,是进行启动实现的入口
    1. 检查当前状态,如果是 CREATE_JUST 则进入启动流程
    2. 检查生产者组名称是否合法
    3. 更改 producerinstanceName
    4. 创建一个 MQClientInstance 类型的 mQClinetFactory 实例
      1. 创建一个新的 defaultMQProducer 实例作为 mQClinetFactory 实例的成员变量
      2. 新的 defaultMQProducer 实例内部又会创建一个新的 defaultMQProducerImpl 实例
      3. 将新的 defaultMQProducer 实例的 ClinetConfig 属性复制粘贴为 producerClinetConfig 属性
    5. producer 实例放入 mQClinetFactoryproducerTable 中,key为 producer 的生产者组名
    6. defaultMQProducercreateTopicKey 作为key,TopicPublishInfo 作为value,放入到 defaultMQProducerImpltopicPublishInfoTable
    7. 启动 mQClinetFactory
      1. 如果没有 NameServer 地址则尝试获取
      2. 启动用于和 broker 通信的 netty 客户端
      3. 启动定时任务
        1. 如果没有指定 NameServer 地址,则定时获取 NameServer 地址
        2. 定期从NameServer更新topic路由信息
        3. 定期清除离线Broker,并向所有Broker发送心跳包
        4. 定时持久化消费者当前消费进度(对MQConsumer有效)
        5. 定时根据当前的积压调优线程池的核心线程数,但是实现是空的
      4. 启动 pullMessageService 从 broker 拉取消息
      5. 启动消费者客户端的负载均衡服务
      6. 启动 mQClinetFactory 内部的 defaultMQProducerImpl 实例
        1. 检查当前状态,如果是 CREATE_JUST 则进入启动流程
        2. 检查生产者组名称是否合法
        3. 由于其 instanceName 等于 MixAll.CLIENT_INNER_PRODUCER_GROUP,因此不更改
        4. 创建一个 MQClientInstance 类型的 mQClinetFactory 实例
        5. producer 实例放入 mQClinetFactoryproducerTable 中,key为 producer 的生产者组名
        6. defaultMQProducercreateTopicKey 作为key,TopicPublishInfo 作为value,放入到 defaultMQProducerImpltopicPublishInfoTable
        7. 将当前状态设置为 RUNNING
        8. 如果上述都成功,则立即发送心跳到所有的 broker
        9. 启动定时任务扫描和处理过期的异步请求
    8. 将当前状态设置为 RUNNING
    9. 如果上述都成功,则立即发送心跳到所有的 broker
    10. 启动定时任务扫描和处理过期的异步请求
  3. 如果 traceDispatch 不为空则启动 traceDispatcher

实例内容

以生产者组名称为 ProducerGroupName,topic名称为 TopicTest 为例,生产者启动成功后的实例内容如下

DefaultMQProducer producer:1 # 启动入口String namesrvAddr = "127.0.0.1:9876" # NameServer地址String clientIp = "192.168.142.1" # producer的ip地址String producerGroup = "ProducerGroupName" # 生产者组名称String createTopicKey = "TBW102" # 用来自动创建topic的topic名称String instanceName = "14896#9822706678400" # 生产者实例的名称DefaultProducerImpl defaultProducerImpl:1 # 实际启动类DefaultMQProducer defaultMQProducer:1 # 就是最开头的 producerConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable:1 = {"TBW102" : TopicPublishInfo} # 存储topic的路由信息MQClientInstance mQClientFactory # 负责管理与 NameServer 和 Broker 的网络连接String clientId = "192.168.142.1@14896#9822706678400" # 为 ip + instanceNameMQClientAPIImpl mQClientAPIImpl # 负责实现与 Broker 之间的通信ConcurrentMap<String, MQProducerInner> producerTable = {"ProducerGroupName" : defaultProducerImpl:1} # MQProducerInner是DefaultProducerImpl的接口ConcurrentMap<String, MQConsumerInner> consumerTable # MQConsumerInner是DefaultConsumerImpl的接口ConcurrentMap<String, HashMap<Long, String>> brokerAddrTable # broker的地址DefaultMQProducer defaultMQProducer:2 # mQClientFactory内部的producer实例String producerGroup = "CLIENT_INNER_PRODUCER"Stirng instanceName = "14896#9822706678400" # 和 producer 的 clientConfig 属性完全一致DefaultProducerImpl defaultProducerImpl:2DefaultMQProducer defaultMQProducer:2ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable:2 = {"TBW102" : TopicPublishInfo} # 存储topic的路由信息

http://www.ppmy.cn/news/999202.html

相关文章

Linux系统防火墙Firewalld

目录 Firewalld概述 Firewalld和iptables的区别 Firewalld网络区域 区域介绍与概念 9个预定义区域 Firewalld数据处理流程 firewalld检查数据包的源地址的规则 Firewalld防火墙的配置方式 常用的firewall-cmd命令选项 服务管理 端口管理 Firewalld概述 Firewalld防火…

做到如下三点,面试拿高薪不是梦

职场的同学&#xff0c;有没有这样的困惑&#xff1f;明明平时能力就不差&#xff0c;为啥面试的薪资就是谈不好&#xff0c;面试表现也不好&#xff0c;如下三点&#xff0c;希望对你有所启发&#xff1a; 1、着重展示自己与目标公司匹配的能力 在面试过程中&#xff0c;往往…

什么是线程?为什么需要线程?和进程的区别?

目录 前言 一.线程是什么&#xff1f; 1.1.为什么需要线程 1.2线程的概念 1.3线程和进程的区别 二.线程的生命周期 三.认识多线程 总结 &#x1f381;个人主页&#xff1a;tq02的博客_CSDN博客-C语言,Java,Java数据结构领域博主 &#x1f3a5; 本文由 tq02 原创&#xf…

自适应巡航控制系统研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 据统计, 我国交通事故造成的伤亡人数每年超过10万人, 其中驾驶员人为原因 (疲劳、酒驾、误操作等) 所致事故逐渐升高.汽车交通…

openGauss学习笔记-28 openGauss 高级数据管理-NULL值

文章目录 openGauss学习笔记-28 openGauss 高级数据管理-NULL值28.1 IS NOT NULL28.2 IS NULL openGauss学习笔记-28 openGauss 高级数据管理-NULL值 NULL值代表未知数据。无法比较NULL和0&#xff0c;因为它们是不等价的。 创建表时&#xff0c;可以指定列可以存放或者不能存…

XML约束和解析

文章目录 概述使用场景语法dtd约束Schema约束解析DOM4j&#xff08;重点&#xff09; 概述 可扩展的标记性语言 使用场景 以前: 传输数据的媒介。 例如&#xff1a;微服务架构中&#xff0c;可以用xml文件进行多语言之间的的联系。 现在: 做配置文件 现在作为传输数据的媒介…

uni iOS 消息推送扩展:实现离线语音播报

文章目录 引言I 前期准备1.1 配置扩展1.2 测试报文II iOS Extension(扩展)2.1 插件作者配置2.2 插件使用者配置see also引言 HBuilderX3.1.5+版本uni原生插件支持iOS Extension(扩展)。 消息推送离线语音播报插件获取方式: 公z号:iOS逆向: 离线包x10, 源码是x15。 实…

ES6学习-Promise

Promise 简单说就是一个容器&#xff0c;里面保存着某个未来才会结束的事件&#xff08;通常是一个异步操作&#xff09;的结果。 语法上&#xff1a; Promise 是一个对象&#xff0c;从它可以获取异步操作的消息。 特点 对象的状态不受外界影响。Promise 对象戴白哦一个异步操…