• [其他] Kafka消息延迟和时序性详解
     一、概括 1.1 介绍 Kafka 消息延迟和时序性 Kafka 消息延迟和时序性对于大多数实时数据流应用程序至关重要。本章将深入介绍这两个核心概念,它们是了解 Kafka 数据流处理的关键要素。  1.1.1 什么是 Kafka 消息延迟? Kafka 消息延迟是指消息从生产者发送到消息被消费者接收之间的时间差。这是一个关键的概念,因为它直接影响到数据流应用程序的实时性和性能。在理想情况下,消息应该以最小的延迟被传递,但在实际情况中,延迟可能会受到多种因素的影响。   消息延迟的因素包括:  网络延迟:消息必须通过网络传输到 Kafka 集群,然后再传输到消费者。网络延迟可能会受到网络拓扑、带宽和路由等因素的影响。  硬件性能:Kafka 集群的硬件性能,包括磁盘、内存和 CPU 的速度,会影响消息的写入和读取速度。  Kafka 内部处理:Kafka 集群的内部处理能力也是一个关键因素。消息必须经过分区、日志段和复制等处理步骤,这可能会引入一些处理延迟。  1.1.2 为什么消息延迟很重要?  消息延迟之所以如此重要,是因为它直接关系到实时数据处理应用程序的可靠性和实时性。在一些应用中,如金融交易处理,甚至毫秒级的延迟都可能导致交易失败或不一致。在监控和日志处理应用中,过高的延迟可能导致数据不准确或失去了时序性。  管理和优化 Kafka 消息延迟是确保应用程序在高负载下仍能快速响应的关键因素。不仅需要了解延迟的来源,还需要采取相应的优化策略。  1.1.3 什么是 Kafka 消息时序性? Kafka 消息时序性是指消息按照它们发送的顺序被接收。这意味着如果消息 A 在消息 B 之前发送,那么消息 A 应该在消息 B 之前被消费。保持消息的时序性对于需要按照时间顺序处理的应用程序至关重要。  维护消息时序性是 Kafka 的一个强大特性。在 Kafka 中,每个分区都可以保证消息的时序性,因为每个分区内的消息是有序的。然而,在多个分区的情况下,时序性可能会受到消费者处理速度不一致的影响,因此需要采取一些策略来维护全局的消息时序性。  1.1.4 消息延迟和时序性的关系 消息延迟和消息时序性之间存在密切的关系。如果消息延迟过大,可能会导致消息失去时序性,因为一条晚到的消息可能会在一条早到的消息之前被处理。因此,了解如何管理消息延迟也包括了维护消息时序性。  在接下来的章节中,我们将深入探讨如何管理和优化 Kafka 消息延迟,以及如何维护消息时序性,以满足实时数据处理应用程序的需求。  1.2 延迟的来源 为了有效地管理和优化 Kafka 消息延迟,我们需要深入了解延迟可能来自哪些方面。下面是一些常见的延迟来源:  1.2.1 Kafka 内部延迟 Kafka 内部延迟是指与 Kafka 内部组件和分区分配相关的延迟。这些因素可能会影响消息在 Kafka 内部的分发、复制和再平衡。  分区分布不均:如果分区分布不均匀,某些分区可能会变得拥挤,而其他分区可能会滞后,导致消息传递延迟。  复制延迟:在 Kafka 中,消息通常会进行复制以确保冗余。复制延迟是指主题的所有副本都能复制消息所需的时间。  再平衡延迟:当 Kafka 集群发生再平衡时,消息的重新分配和复制可能导致消息传递延迟。  二、衡量和监控消息延迟 在本节中,我们将深入探讨如何度量和监控 Kafka 消息延迟,这将帮助你更好地了解问题并采取相应的措施来提高延迟性能。  2.1 延迟的度量 为了有效地管理 Kafka 消息延迟,首先需要能够度量它。下面是一些常见的延迟度量方式:  2.1.1 生产者到 Kafka 延迟 这是指消息从生产者发送到 Kafka 集群之间的延迟。为了度量这一延迟,你可以采取以下方法:  记录发送时间戳:在生产者端,记录每条消息的发送时间戳。一旦消息成功写入 Kafka,记录接收时间戳。然后,通过将这两个时间戳相减,你可以获得消息的生产者到 Kafka 的延迟。  以下是如何记录发送和接收时间戳的代码示例:  // 记录消息发送时间戳 long sendTimestamp = System.currentTimeMillis(); ProducerRecord<String, String> record = new ProducerRecord<>("my_topic", "key", "value"); producer.send(record, (metadata, exception) -> {     if (exception == null) {         long receiveTimestamp = System.currentTimeMillis();         long producerToKafkaLatency = receiveTimestamp - sendTimestamp;         System.out.println("生产者到 Kafka 延迟:" + producerToKafkaLatency + " 毫秒");     } else {         System.err.println("消息发送失败: " + exception.getMessage());     } }); 1 2 3 4 5 6 7 8 9 10 11 12 2.1.2 Kafka 内部延迟 Kafka 内部延迟是指消息在 Kafka 集群内部传递的延迟。你可以使用 Kafka 内置度量来度量它,包括:  Log End-to-End Latency:这是度量消息从生产者发送到消费者接收的总延迟。它包括了网络传输、分区复制、再平衡等各个环节的时间。  以下是一个示例:  // 创建 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);  // 订阅主题 consumer.subscribe(Collections.singletonList("my_topic"));  while (true) {     ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));     for (ConsumerRecord<String, String> record : records) {         long endToEndLatency = record.timestamp() - record.timestampType().createTimestamp();         System.out.println("Log End-to-End 延迟:" + endToEndLatency + " 毫秒");     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 2.1.3 消费者处理延迟 消费者处理延迟是指消息从 Kafka 接收到被消费者实际处理的时间。为了度量这一延迟,你可以采取以下方法:  记录消费时间戳:在消费者端,记录每条消息的接收时间戳和处理时间戳。通过计算这两个时间戳的差值,你可以得到消息的消费者处理延迟。 以下是如何记录消费时间戳的代码示例:  KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); consumer.subscribe(Collections.singletonList("my_topic"));  while (true) {     ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));     for (ConsumerRecord<String, String> record : records) {         long receiveTimestamp = System.currentTimeMillis();         long consumerProcessingLatency = receiveTimestamp - record.timestamp();         System.out.println("消费者处理延迟:" + consumerProcessingLatency + " 毫秒");     } } 1 2 3 4 5 6 7 8 9 10 11 2.2 监控和度量工具 在度量和监控 Kafka 消息延迟时,使用适当的工具和系统是至关重要的。下面是一些工具和步骤,帮助你有效地监控 Kafka 消息延迟,包括代码示例:  2.2.1 Kafka 内置度量 Kafka 提供了内置度量,可通过多种方式来监控。以下是一些示例,演示如何通过 Kafka 的 JMX 界面访问这些度量:  使用 JConsole 直接连接到 Kafka Broker:  启动 Kafka Broker。 打开 JConsole(Java 监控与管理控制台)。 在 JConsole 中选择 Kafka Broker 进程。 导航到 “kafka.server” 和 “kafka.consumer”,以查看各种度量。 使用 Jolokia(Kafka JMX HTTP Bridge):  启用 Jolokia 作为 Kafka Broker 的 JMX HTTP Bridge。 使用 Web 浏览器或 HTTP 请求访问 Jolokia 接口来获取度量数据。例如,使用 cURL 进行 HTTP GET 请求: curl http://localhost:8778/jolokia/read/kafka.server:name=BrokerTopicMetrics/TotalFetchRequestsPerSec 1 这将返回有关 Kafka Broker 主题度量的信息。  2.2.2 第三方监控工具 除了 Kafka 内置度量,你还可以使用第三方监控工具,如 Prometheus 和 Grafana,来收集、可视化和警报度量数据。以下是一些步骤:  配置 Prometheus:  部署和配置 Prometheus 服务器。 创建用于监控 Kafka 的 Prometheus 配置文件,定义抓取度量数据的频率和目标。 启动 Prometheus 服务器。 设置 Grafana 仪表板:  部署和配置 Grafana 服务器。 在 Grafana 中创建仪表板,使用 Prometheus 作为数据源。 添加度量查询,配置警报规则和可视化图表。 可视化 Kafka 延迟数据:  在 Grafana 仪表板中,你可以设置不同的图表来可视化 Kafka 延迟数据,例如生产者到 Kafka 延迟、消费者处理延迟等。通过设置警报规则,你还可以及时收到通知,以便采取行动。  2.2.3 配置和使用监控工具 为了配置和使用监控工具,你需要执行以下步骤:  定义度量指标:确定你要度量的关键度量指标,如生产者到 Kafka 延迟、消费者处理延迟等。  设置警报规则:为了快速响应问题,设置警报规则,以便在度量数据超出预定阈值时接收通知。  创建可视化仪表板:使用监控工具(如 Grafana)创建可视化仪表板,以集中展示度量数据并实时监测延迟情况。可配置的图表和仪表板有助于更好地理解数据趋势。  以上步骤和工具将帮助你更好地度量和监控 Kafka 消息延迟,以及及时采取行动来维护系统的性能和可靠性。  三、降低消息延迟 既然我们了解了 Kafka 消息延迟的来源以及如何度量和监控它,让我们继续探讨如何降低消息延迟。以下是一些有效的实践方法,可以帮助你减少 Kafka 消息延迟:  3.1 优化 Kafka 配置 3.1.1 Producer 和 Consumer 参数 生产者参数示例: # 生产者参数示例 acks=all compression.type=snappy linger.ms=20 max.in.flight.requests.per.connection=1 1 2 3 4 5 acks 设置为 all,以确保生产者等待来自所有分区副本的确认。这提高了可靠性,但可能增加了延迟。 compression.type 使用 Snappy 压缩消息,减小了网络传输延迟。 linger.ms 设置为 20 毫秒,以允许生产者在发送消息之前等待更多消息。这有助于减少短暂的消息发送延迟。 max.in.flight.requests.per.connection 设置为 1,以确保在收到分区副本的确认之前不会发送新的消息。 消费者参数示例: # 消费者参数示例 max.poll.records=500 fetch.min.bytes=1 fetch.max.wait.ms=100 enable.auto.commit=false 1 2 3 4 5 max.poll.records 设置为 500,以一次性拉取多条消息,提高吞吐量。 fetch.min.bytes 设置为 1,以确保即使没有足够数据,也立即拉取消息。 fetch.max.wait.ms 设置为 100 毫秒,以限制拉取消息的等待时间。 enable.auto.commit 禁用自动提交位移,以确保精确控制消息的确认。 3.1.2 Broker 参数 优化 Kafka broker 参数可以提高整体性能。以下是示例:  # Kafka Broker 参数示例 num.network.threads=3 num.io.threads=8 log.segment.bytes=1073741824 log.retention.check.interval.ms=300000 1 2 3 4 5 num.network.threads 和 num.io.threads 设置为适当的值,以充分利用硬件资源。 log.segment.bytes 设置为 1 GB,以充分利用磁盘性能。 log.retention.check.interval.ms 设置为 300,000 毫秒,以降低清理日志段的频率。 3.1.3 Topic 参数 优化每个主题的参数以满足应用程序需求也很重要。以下是示例:  # 创建 Kafka 主题并设置参数示例 kafka-topics.sh --create --topic my_topic --partitions 8 --replication-factor 2 --config cleanup.policy=compact 1 2 --partitions 8 设置分区数量为 8,以提高并行性。 --replication-factor 2 设置复制因子为 2,以提高可靠性。 --config cleanup.policy=compact 设置清理策略为压缩策略,以减小数据保留成本。 通过适当配置这些参数,你可以有效地优化 Kafka 配置以降低消息延迟并提高性能。请根据你的应用程序需求和硬件资源进行调整。  3.2 编写高效的生产者和消费者 最后,编写高效的 Kafka 生产者和消费者代码对于降低延迟至关重要。以下是一些最佳实践:  3.2.1 生产者最佳实践 使用异步发送:将多个消息批量发送,而不是逐条发送。这可以减少网络通信的次数,提高吞吐量。  使用 Kafka 生产者的缓冲机制:充分利用 Kafka 生产者的缓冲功能,以减少网络通信次数。  使用分区键:通过选择合适的分区键,确保数据均匀分布在不同的分区上,从而提高并行性。  3.2.2 消费者最佳实践 使用多线程消费:启用多个消费者线程,以便并行处理消息。这可以提高处理能力和降低延迟。  调整消费者参数:调整消费者参数,如 fetch.min.bytes 和 fetch.max.wait.ms,以平衡吞吐量和延迟。  使用消息批处理:将一批消息一起处理,以减小处理开销。  3.2.3 数据序列化 选择高效的数据序列化格式对于降低数据传输和存储开销很重要。以下是一些建议的格式:  Avro:Apache Avro 是一种数据序列化框架,具有高度压缩和高性能的特点。它适用于大规模数据处理。  Protocol Buffers:Google Protocol Buffers(ProtoBuf)是一种轻量级的二进制数据格式,具有出色的性能和紧凑的数据表示。  四、Kafka 消息时序性 消息时序性是大多数实时数据流应用程序的核心要求。在本节中,我们将深入探讨消息时序性的概念、为何它如此重要以及如何保障消息时序性。  4.1 什么是消息时序性? 消息时序性是指消息按照它们发送的顺序被接收和处理的特性。在 Kafka 中,每个分区内的消息是有序的,这意味着消息以它们被生产者发送的顺序排列。然而,跨越多个分区的消息需要额外的工作来保持它们的时序性。  4.1.1 为何消息时序性重要? 消息时序性对于许多应用程序至关重要,特别是需要按照时间顺序处理数据的应用。以下是一些应用领域,消息时序性非常关键:  金融领域:在金融交易中,确保交易按照它们发生的确切顺序进行处理至关重要。任何失去时序性的交易可能会导致不一致性或错误的交易。  日志记录:在日志记录和监控应用程序中,事件的时序性对于分析和排查问题非常关键。失去事件的时序性可能会导致混淆和数据不准确。  电商应用:在线商店的订单处理需要确保订单的创建、支付和发货等步骤按照正确的顺序进行,以避免订单混乱和不准确。  4.2 保障消息时序性 在分布式系统中,保障消息时序性可能会面临一些挑战,特别是在跨越多个分区的情况下。以下是一些策略和最佳实践,可帮助你确保消息时序性:  4.2.1 分区和消息排序 使用合适的分区策略对消息进行排序,以确保相关的消息被发送到同一个分区。这样可以维护消息在单个分区内的顺序性。对于需要按照特定键排序的消息,可以使用自定义分区器来实现。  以下是如何使用合适的分区策略对消息进行排序的代码示例:  // 自定义分区器,确保相关消息基于特定键被发送到同一个分区 public class CustomPartitioner implements Partitioner {     @Override     public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {         // 在此处根据 key 的某种规则计算分区编号         // 例如,可以使用哈希函数或其他方法         int numPartitions = cluster.partitionsForTopic(topic).size();         return Math.abs(key.hashCode()) % numPartitions;     }      @Override     public void close() {         // 可选的资源清理     }      @Override     public void configure(Map<String, ?> configs) {         // 可选的配置     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 4.2.2 数据一致性 确保生产者发送的消息是有序的。这可能需要在应用程序层面实施,包括对消息进行缓冲、排序和合并,以确保它们按照正确的顺序发送到 Kafka。  以下是如何确保数据一致性的代码示例:  // 生产者端的消息排序 ProducerRecord<String, String> record1 = new ProducerRecord<>("my-topic", "key1", "message1"); ProducerRecord<String, String> record2 = new ProducerRecord<>("my-topic", "key2", "message2");  // 发送消息 producer.send(record1); producer.send(record2);  // 消费者端保证消息按照键排序 ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) {     // 处理消息,确保按照键的顺序进行 } 1 2 3 4 5 6 7 8 9 10 11 12 13 4.2.3 消费者并行性 在消费者端,使用适当的线程和分区分配来确保消息以正确的顺序处理。这可能涉及消费者线程数量的管理以及确保每个线程只处理一个分区,以避免顺序混乱。  以下是如何确保消费者并行性的代码示例:  // 创建具有多个消费者线程的 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");  // 创建 Kafka 消费者 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);  // 订阅主题 consumer.subscribe(Collections.singletonList("my-topic"));  // 创建多个消费者线程 int numThreads = 3; for (int i = 0; i < numThreads; i++) {     Runnable consumerThread = new ConsumerThread(consumer);     new Thread(consumerThread).start(); }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 五、总结 在本篇技术博客中,我们深入探讨了 Kafka 消息延迟和时序性的重要性以及如何度量、监控和降低消息延迟。我们还讨论了消息时序性的挑战和如何确保消息时序性。对于构建实时数据流应用程序的开发人员来说,深入理解这些概念是至关重要的。通过合理配置 Kafka、优化网络和硬件、编写高效的生产者和消费者代码,以及维护消息时序性,你可以构建出高性能和可靠的数据流系统。  无论你的应用是金融交易、监控、日志记录还是其他领域,这些建议和最佳实践都将帮助你更好地处理 Kafka 消息延迟和时序性的挑战,确保数据的可靠性和一致性。  六、从零开始学架构:照着做,你也能成为架构师  1、内容介绍 京东购买链接:从零开始学架构:照着做,你也能成为架构师  本书的内容主要包含以下几部分:  架构设计基础,包括架构设计相关概念、历史、原则、基本方法,让架构设计不再神秘; 架构设计流程,通过一个虚拟的案例,描述了一个通用的架构设计流程,让架构设计不再依赖天才的创作,而是有章可循; 架构设计专题:包括高性能架构设计、高可用架构设计、可扩展架构设计,这些模式可以直接参考和应用; 架构设计实战,包括重构、开源方案引入、架构发展路径、互联网架构模板等 ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/135060020 
  • [其他] SimpleDateFormat类为何不是线程安全的?
    提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。重现SimpleDateFormat类的线程安全问题为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。有关CountDownLatch类和Semaphore类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。而Semaphore类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 测试SimpleDateFormat的线程不安全问题 */ public class SimpleDateFormatTest01 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。simpleDateFormat.parse("2020-01-01");当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。运行程序输出的结果信息如下所示。Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败 线程:pool-1-thread-9 格式化日期失败 线程:pool-1-thread-10 格式化日期失败 Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败 线程:pool-1-thread-21 格式化日期失败 Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败 线程:pool-1-thread-11 格式化日期失败 java.lang.ArrayIndexOutOfBoundsException 线程:pool-1-thread-27 格式化日期失败 at java.lang.System.arraycopy(Native Method) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597) at java.lang.StringBuffer.append(StringBuffer.java:367) at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 线程:pool-1-thread-14 格式化日期失败 at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) 线程:pool-1-thread-13 格式化日期失败 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) Process finished with exit code 1说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。SimpleDateFormat类为何不是线程安全的?那么,接下来,我们就一起来看看真正引起SimpleDateFormat类线程不安全的根本原因。通过查看SimpleDateFormat类的源码,我们得知:SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量,如下所示。/** * The {@link Calendar} instance used for calculating the date-time fields * and the instant of time. This field is used for both formatting and * parsing. * * <p>Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * <code>DateFormat</code>. * @serial */ protected Calendar calendar;从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。接下来,我们再查看parse()方法接近最后的部分。@Override public Date parse(String text, ParsePosition pos){ ################此处省略N行代码################## Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { pos.errorIndex = start; pos.index = oldStart; return null; } return parsedDate; }可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。接下来,我们再来看看CalendarBuilder.establish()方法,如下所示。Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } cal.clear(); // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } if (weekDate) { int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; int dayOfWeek = isSet(DAY_OF_WEEK) ? field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek(); if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) { if (dayOfWeek >= 8) { dayOfWeek--; weekOfYear += dayOfWeek / 7; dayOfWeek = (dayOfWeek % 7) + 1; } else { while (dayOfWeek <= 0) { dayOfWeek += 7; weekOfYear--; } } dayOfWeek = toCalendarDayOfWeek(dayOfWeek); } cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek); } return cal; }在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。那么,得知了SimpleDateFormat类不是线程安全的,以及造成SimpleDateFormat类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决SimpleDateFormat类在高并发场景下的线程安全问题。解决SimpleDateFormat类的线程安全问题解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。1.局部变量法最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 局部变量法解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest02 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时运行修改后的程序,输出结果如下所示。所有线程格式化日期成功至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。2.synchronized锁方式将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest03 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); } } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时,解决问题的关键代码如下所示。synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); }运行程序,输出结果如下所示。所有线程格式化日期成功需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。3.Lock锁方式Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author binghe * @version 1.0.0 * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest04 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); //Lock对象 private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { lock.lock(); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }finally { lock.unlock(); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。finally { lock.unlock(); }运行程序,输出结果如下所示。所有线程格式化日期成功此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。4.ThreadLocal方式使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest05 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { threadLocal.get().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。运行程序,输出结果如下所示。所有线程格式化日期成功此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest06 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); private static DateFormat getDateFormat(){ DateFormat dateFormat = threadLocal.get(); if(dateFormat == null){ dateFormat = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(dateFormat); } return dateFormat; } public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { getDateFormat().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }5.DateTimeFormatter方式DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。package io.binghe.concurrent.lab06; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest07 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { LocalDate.parse("2020-01-01", formatter); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。运行程序,输出结果如下所示。所有线程格式化日期成功使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。6.joda-time方式joda-time是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。这里,以Maven为例,如下所示引入joda-time库。<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>引入joda-time库后,实现的程序代码如下所示。package io.binghe.concurrent.lab06; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest08 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }这里,需要注意的是:DateTime类是org.joda.time包下的类,DateTimeFormat类和DateTimeFormatter类都是org.joda.time.format包下的类,如下所示。import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;运行程序,输出结果如下所示。所有线程格式化日期成功使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。解决SimpleDateFormat类的线程安全问题的方案总结综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。来自:https://zhuanlan.zhihu.com/p/395751163
  • [其他] java线程池有哪些类型
    Java中主要有四种类型的线程池,它们分别是:可缓存线程池:通过Executors.newCachedThreadPool()创建,这种线程池会根据需要创建新线程,但同时会重用空闲的线程。如果线程池中的线程超过60秒未被使用,则会被终止并移除,这样可以避免资源浪费。固定线程池:通过Executors.newFixedThreadPool(int nThreads)创建,这种线程池的特点是核心线程数和最大线程数相同,适用于执行长期任务且任务数量固定的情况。定时线程池:通过Executors.newScheduledThreadPool(int corePoolSize)创建,适用于需要周期性执行任务的场景,如定时任务、定时扫描等。单线程化线程池:通过Executors.newSingleThreadExecutor()创建,这种线程池只有一个工作线程,适用于需要保证任务按顺序执行的场景。这些线程池都是ExecutorService接口的实现类,它们各自有不同的特点和适用场景。在实际开发中,选择合适的线程池类型可以提高程序的性能和响应速度。
  • [其他] java中如何设计跨线程的数据同步问题
    在Java中,跨线程的数据同步问题可以通过以下几种方式来解决:使用synchronized关键字:通过在方法或代码块上添加synchronized关键字,可以确保同一时间只有一个线程能够访问共享资源。例如:public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int getCount() { return count; } }使用ReentrantLock类:ReentrantLock是一个可重入的互斥锁,它提供了与synchronized相同的基本行为和语义,但具有更高的灵活性。例如:import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public void decrement() { lock.lock(); try { count--; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }使用Semaphore类:Semaphore是一个计数信号量,可以用来控制同时访问某个资源的线程数量。例如:import java.util.concurrent.Semaphore; public class SemaphoreExample { private final Semaphore semaphore = new Semaphore(1); private int count = 0; public void increment() throws InterruptedException { semaphore.acquire(); try { count++; } finally { semaphore.release(); } } public void decrement() throws InterruptedException { semaphore.acquire(); try { count--; } finally { semaphore.release(); } } public int getCount() throws InterruptedException { semaphore.acquire(); try { return count; } finally { semaphore.release(); } } }使用Atomic类:Atomic类提供了一种无锁的方式来实现线程安全的操作。例如:import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public void decrement() { count.decrementAndGet(); } public int getCount() { return count.get(); } }以上四种方法都可以解决Java中跨线程的数据同步问题,具体选择哪种方法取决于实际需求和场景。
  • [其他] java中对Long判空Object.isNull和!=null的区别
    在Java中,对Long类型的变量进行判空时,可以使用Objects.isNull()方法和!= null两种方式。它们的区别如下:Objects.isNull()方法:这是Java 8引入的一个工具类方法,用于判断一个对象是否为null。它接受一个参数,如果参数为null,则返回true,否则返回false。这个方法主要用于处理包装类型(如Long、Integer等)的null值判断。import java.util.Objects; public class Main { public static void main(String[] args) { Long num = null; if (Objects.isNull(num)) { System.out.println("num is null"); } else { System.out.println("num is not null"); } } }!= null:这是Java中常见的判空方式,用于判断一个对象是否为null。如果对象不等于null,则表示对象不为空。这种方式适用于所有类型的对象,包括基本类型和引用类型。public class Main { public static void main(String[] args) { Long num = null; if (num != null) { System.out.println("num is not null"); } else { System.out.println("num is null"); } } }总结:Objects.isNull()方法主要用于处理包装类型的null值判断,而!= null方式适用于所有类型的对象。在实际使用中,可以根据需要选择合适的判空方式。
  • [专题汇总] 2024年3月技术干货专题汇总来啦 。速进
     大家好,三月的合集又来了,本次涵盖了java,mysql,spirngboot,oracle,nginx,webpack,css,python,mongoDB,devops,golang诸多内容供大家学习。 1.Python中数据解压缩的技巧分享【转】 https://bbs.huaweicloud.com/forum/thread-0274147063634001023-1-1.html  2.CSS如何设置背景模糊周边有白色光晕(解决方案)【转】 https://bbs.huaweicloud.com/forum/thread-02121147063476589021-1-1.html  3.CSS实现渐变式圆点加载动画【转】 https://bbs.huaweicloud.com/forum/thread-0274147063231589021-1-1.html  4. Nginx access.log日志详解及统计分析小结【转】 https://bbs.huaweicloud.com/forum/thread-0274147062581229019-1-1.html  5.Webpack部署本地服务器的方法【转】 https://bbs.huaweicloud.com/forum/thread-02110147062400691022-1-1.html  6.Nginx漏洞整改实现限制IP访问&隐藏nginx版本信息【转】 https://bbs.huaweicloud.com/forum/thread-0273147062364276013-1-1.html  7.Nginx加固的几种方式(控制超时时间&限制客户端下载速度&并发连接数)【转】 https://bbs.huaweicloud.com/forum/thread-02109147062305049017-1-1.html  8.Nginx配置http和https的实现步骤【转】 https://bbs.huaweicloud.com/forum/thread-0274147061484998017-1-1.html  9.MongoDB内存过高问题分析及解决【转】 https://bbs.huaweicloud.com/forum/thread-0276147061392824019-1-1.html  10.Oracle数据库中字符串截取最全方法总结【转】 https://bbs.huaweicloud.com/forum/thread-0240147061327119025-1-1.html  11.MySQL数据库如何克隆(带脚本)【转】 https://bbs.huaweicloud.com/forum/thread-0294147061178556014-1-1.html  12.mysql5.6建立索引报错1709问题及解决【转】 https://bbs.huaweicloud.com/forum/thread-0240147061135949024-1-1.html  13.修改Mysql索引长度限制解决767 byte限制问题【转】 https://bbs.huaweicloud.com/forum/thread-0240147060706517023-1-1.html  14.SQL实现模糊查询的四种方法小结【转】 https://bbs.huaweicloud.com/forum/thread-02121147060465820020-1-1.html  15.MySql查询中按多个字段排序的方法【转】 https://bbs.huaweicloud.com/forum/thread-0240147060411844022-1-1.html  16. Devops-01-devops 是什么?【转】 https://bbs.huaweicloud.com/forum/thread-02127146828359305034-1-1.html  17.使用 Java 在Excel中创建下拉列表【转】 https://bbs.huaweicloud.com/forum/thread-0297146827870477028-1-1.html  18.管理与控制平面设计 https://bbs.huaweicloud.com/forum/thread-0239146030318575010-1-1.html  19.cisco https://bbs.huaweicloud.com/forum/thread-0292146030276181005-1-1.html  20. NSX-V整体架构 https://bbs.huaweicloud.com/forum/thread-02127146028999196007-1-1.html  21.从NVP到NSX https://bbs.huaweicloud.com/forum/thread-0279146028887825006-1-1.html  22.【监控】spring actuator源码速读-转载 https://bbs.huaweicloud.com/forum/thread-0239145954118788007-1-1.html  23.SpringCloud-RabbitMQ消息模型-转载 https://bbs.huaweicloud.com/forum/thread-0282145954036229004-1-1.html  24.【Golang入门教程】Go语言变量的声明-转载 https://bbs.huaweicloud.com/forum/thread-0279145953985911003-1-1.html  25. Spring Boot 3核心技术与最佳实践-转载 https://bbs.huaweicloud.com/forum/thread-0239145953918959006-1-1.html 
  • [分享交流] java22发布了,你们公司现在都在用哪个版本开发呢?
    java22发布了,你们公司现在都在用哪个版本开发呢?
  • [技术干货] IntelliJ IDEA 常用快捷键一览表
    1-IDEA的日常快捷键 第1组:通用型 说明    快捷键 复制代码-copy    ctrl + c 粘贴-paste    ctrl + v 剪切-cut    ctrl + x 撤销-undo    ctrl + z 反撤销-redo    ctrl + shift + z 保存-save all    ctrl + s 全选-select all    ctrl + a 第2组:提高编写速度(上) 说明    快捷键 智能提示-edit    alt + enter 提示代码模板-insert live template    ctrl+j 使用xx块环绕-surround with ...    ctrl+alt+t 调出生成getter/setter/构造器等结构-generate ...    alt+insert 自动生成返回值变量-introduce variable ...    ctrl+alt+v 复制指定行的代码-duplicate line or selection    ctrl+d 删除指定行的代码-delete line    ctrl+y 切换到下一行代码空位-start new line    shift + enter 切换到上一行代码空位-start new line before current    ctrl +alt+ enter 向上移动代码-move statement up    ctrl+shift+↑ 向下移动代码-move statement down    ctrl+shift+↓ 向上移动一行-move line up    alt+shift+↑ 向下移动一行-move line down    alt+shift+↓ 方法的形参列表提醒-parameter info    ctrl+p 第3组:提高编写速度(下) 说明    快捷键 批量修改指定的变量名、方法名、类名等-rename    shift+f6 抽取代码重构方法-extract method ...    ctrl+alt+m 重写父类的方法-override methods ...    ctrl+o 实现接口的方法-implements methods ...    ctrl+i 选中的结构的大小写的切换-toggle case    ctrl+shift+u 批量导包-optimize imports    ctrl+alt+o 第4组:类结构、查找和查看源码 说明    快捷键 如何查看源码-go to class...    ctrl + 选中指定的结构 或 ctrl+n 显示当前类结构,支持搜索指定的方法、属性等-file structure    ctrl+f12 退回到前一个编辑的页面-back    ctrl+alt+← 进入到下一个编辑的页面-forward    ctrl+alt+→ 打开的类文件之间切换-select previous/next tab    alt+←/→ 光标选中指定的类,查看继承树结构-Type Hierarchy    ctrl+h 查看方法文档-quick documentation    ctrl+q 类的UML关系图-show uml popup    ctrl+alt+u 定位某行-go to line/column    ctrl+g 回溯变量或方法的来源-go to implementation(s)    ctrl+alt+b 折叠方法实现-collapse all    ctrl+shift+ - 展开方法实现-expand all    ctrl+shift+ + 第5组:查找、替换与关闭 说明    快捷键 查找指定的结构    ctlr+f 快速查找:选中的Word快速定位到下一个-find next    ctrl+l 查找与替换-replace    ctrl+r 直接定位到当前行的首位-move caret to line start    home 直接定位到当前行的末位 -move caret to line end    end 查询当前元素在当前文件中的引用,然后按 F3 可以选择    ctrl+f7 全项目搜索文本-find in path ...    ctrl+shift+f 关闭当前窗口-close    ctrl+f4 第6组:调整格式 说明    快捷键 格式化代码-reformat code    ctrl+alt+l 使用单行注释-comment with line comment    ctrl + / 使用/取消多行注释-comment with block comment    ctrl + shift + / 选中数行,整体往后移动-tab    tab 选中数行,整体往前移动-prev tab    shift + tab 2-Debug快捷键 说明    快捷键 单步调试(不进入函数内部)- step over    F8 单步调试(进入函数内部)- step into    F7 强制单步调试(进入函数内部) - force step into    alt+shift+f7 选择要进入的函数 - smart step into    shift + F7 跳出函数 - step out    shift + F8 运行到断点 - run to cursor    alt + F9 继续执行,进入下一个断点或执行完程序 - resume program    F9 停止 - stop    Ctrl+F2 查看断点 - view breakpoints    Ctrl+Shift+F8 关闭 - close    Ctrl+F4 ————————————————           原文链接:https://blog.csdn.net/m0_59281987/article/details/137051712 
  • [技术干货] Idea与Maven版本不一致问题
    Idea拉取Jar包,报Unable to import maven project: See logs for details 查看日志信息No implementation for org.apache.maven.model.path.PathTranslator was bound  解决办法:  IDEA执行Maven报错 Unable to import maven project: See logs for details Help -> Show Log in Explorer 查看详细的错误日志  1) No implementation for org.apache.maven.model.path.PathTranslator was bound. while locating org.apache.maven.model.path.PathTranslator for field at org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator.pathTranslator(Unknown Source) at org.codehaus.plexus.DefaultPlexusContainer$1.configure(DefaultPlexusContainer.java:350) 2) No implementation for org.apache.maven.model.path.UrlNormalizer was bound. while locating org.apache.maven.model.path.UrlNormalizer for field at org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator.urlNormalizer(Unknown Source) at org.codehaus.plexus.DefaultPlexusContainer$1.configure(DefaultPlexusContainer.java:350)  解决办法 Maven与IDEA版本不一致导致: 当前:Maven-3.6.3 IDEA2019.01 修改Maven版本为:3.5.4 https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.5.4/binaries/ 最后:修改maven在IDEA中的版本配置即可解决。 ————————————————    原文链接:https://blog.csdn.net/qq_31016939/article/details/137032201 
  • [技术干货] idea中 VM options配置
    VM options这项需要我们自己填写,我填的是VM options : -Xms256m -Xmx256m -XX:PermSize=32M -XX:MaxPermSize=128m  每一项以空格隔开 参数说明 -Xms256m:设置JVM初始堆内存为256m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -Xmx256m:设置JVM最大堆内存为256m。 -Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。  -Xmn2g:设置年轻代大小为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。 -XX:NewSize=1024m:设置年轻代初始值为1024M。 -XX:MaxNewSize=1024m:设置年轻代最大值为1024M。 -XX:PermSize=256m:设置持久代初始值为256M。 -XX:MaxPermSize=256m:设置持久代最大值为256M。 -XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。 -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。  -XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。  标准参数,所有JVM都必须支持这些参数的功能,而且向后兼容;例如: -client——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。 -server——设置JVM使Server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容; 非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用; ———————————————— 原文链接:https://blog.csdn.net/style_L_/article/details/129079538 
  • [技术干货] 新版idea如何开启多台JVM虚拟机
    流程如下: 打开 IntelliJ IDEA: 启动你的 IntelliJ IDEA。 打开新的项目或窗口: 可以打开一个新的项目,或者在当前项目中打开一个新的窗口。 配置新的运行/调试配置: 在 IntelliJ IDEA 的工具栏中,找到并点击右侧的编辑配置按钮(位于运行/调试配置的旁边,通常是一个小下拉箭头)。 选择 Edit Configurations: 在弹出的菜单中,选择 "Edit Configurations"。 添加新的配置: 在打开的配置窗口左上角,点击加号按钮或复制按钮,以添加一个新的配置。 选择 Application 配置类型: 在弹出的菜单中,选择 "Application" 配置类型。 配置新的运行/调试配置: 在右侧的配置窗口中,配置以下参数: Name(名称): 为你的配置起一个名称,以便区分。 Main class(主类): 指定你的 Java 主类。 VM options(虚拟机选项): 在这里输入你想要配置的虚拟机选项,比如 -Duser.language=en。 Program arguments(程序参数): 如果你的程序需要参数,可以在这里添加。 保存配置: 确保点击窗口底部的 "OK" 按钮来保存你的配置。 运行新的配置: 在 IntelliJ IDEA 的工具栏中选择你刚刚配置的运行/调试配置,点击运行按钮。 通过以上步骤,你就可以在 IntelliJ IDEA 中同时运行多个不同的 JVM 虚拟机实例。你可以通过创建多个配置,每个配置对应一个虚拟机实例,方便地管理和切换。这在需要模拟多个环境或进行并发调试时非常有用。 手把手教学 1.看看自己的项目 2.可能开始的时候啥也没有,就点Run Configuration Type 3.再点击Edit Configurations... 4.点击+号添加SpringBoot 5.主类选择一下,一般就一个,点他选了就行。 6.然后点击Modify Options 选择添加add VM Options 7.点击apply 再OK就有了!!! ————————————————            原文链接:https://blog.csdn.net/m0_62645012/article/details/134628159 
  • [技术干货] IDEA设置多个JVM运行参数
    格式 以-D开头,多个jvm运行参数 用空格隔开 -Dproperty=Value 例如-Dfile.encoding=UTF-8 -Dspring.profiles.active=dev 注意: 1、如果-Dproperty=value的value中包含空格,可以将value使用引号引起来。例如:-Dmyname=“hello world”。 2、如果配置了-Dproperty=value参数,又在程序中使用了System.setProperty对同一个变量进行设置,那么以程序中的设置为准。 优先级关系 代码中的配置>Application中的配置>全局配置 ———————————————— 原文链接:https://blog.csdn.net/iijik55/article/details/126384345 
  • [技术干货] vmoptions默认配置_VM options 以及 properties文件的一些理解
    背景 之前做项目配数据源信息,一般是把用户名,密码这些都放在一个properties文件中。最近做了个项目是把用户名密码配置在了web容器(tomcat)启动选项里,然后通过${key}注入到properties文件中。 由此产生很多疑问 容器的启动选项是怎么注入到properties文件中的。 容器的启动选项设置的属性的使用范围。 properties中配置的属性是怎么注入到spring配置文件中的。 properties中配置的属性的使用范围。 个人理解 (有问题请指教) 在网上找了很久,没有找到说清楚VM options是什么的相关帖子。我就自己理解一下吧。 写了一个demo,最后发现在VM options中配置的参数,可以通过${}占位符在项目的任何位置注入,比如spring的xml配置文件,properties文件以及通过@Value注入java文件中。 可以理解为容器的启动选项配置的属性是整个web容器的全局变量,可以在容器中的所有项目,所有项目中的所有文件中通过${key}引用到。 关于properties文件 紧接着我顺带深入理解了一下properties文件以及properties中定义的属性在spring中的作用域。 基础操作 首先properties就是一个定义键值对的文件,我们可以通过Properties类加载然后通过getProperty("key")来获取value,参考 通常做法: 但是我们用的最多的是在spring的相关配置中使用properties来收集一些配置属性方便修改。 在spring的xml配置文件中使用 来加载properties文件,然后在xml中用${key}来注入对应value,或者在java类属性上用@Value(${key})来注入对应value。 于是我又产生了一个疑问:在xml中引入的properties文件的作用域是什么,是当前xml文件?是spring容器?还是整个项目? 写了一个demo测了一下,发现xml中引入的properties的属性是容器级别的。比如springmvc系统中会有springmvc子容器和spring核心父容器两个容器,在springmvc.xml中引入的properties的属性只能在springmvc子容器中的bean以及同级别xml配置文件中获取到,而spring父容器中的bean则获取不到,反之亦然。 总结: 容器的启动选项是怎么注入到properties文件中的。解答:我们只需要配置容器启动选项,其他完全由容器实现和处理。 容器的启动选项设置的属性的使用范围。解答:容器中所有项目,项目中所有文件。 properties中配置的属性是怎么注入到spring配置文件中的。解答:我们只需要引入properties文件,其他由spring实现和处理 properties中配置的属性的使用范围。解答:属性引入的容器 ————————————————                   原文链接:https://blog.csdn.net/weixin_35186171/article/details/113548243 
  • [技术干货] Jetbrains 全家桶 配置
    1:建议下载软件Everything 2:准备好Jetbrains家的相关IDE下载链接:jetbrains.com【安装过程不描述了】 3:建议下载付费的!没钱买正版可以去淘宝1r买激活码几乎全家桶适用,或者找人要 4:安装完后不要打开、否则自动生成配置文件在系统盘!先修改配置文件 二:配置文件 #这个配置可以减少默认在系统盘占用大量内存 1:在一个内存足够的磁盘创建文件夹【随便取,要英文】 2:搜索并打开文件idea.properties 3:分别有四个关键字且有对应的路径 idea.config.path、idea.system.path、idea.plugins.path、idea.log.path #找到第一步创建好的文件夹的路径、替换四个关键字对应的路径。 #注意不要有反斜杠\和中文,若修改当前行数的一行有#号记得删掉,否则为注释不做处理 4:配置成功三:按键配置 #此按键布局的移动键位和Vim编辑器操作有点类似【个人习惯】 #习惯后就不太需要鼠标了 1:打开IDE---Setting---Keymap---右边齿轮---Duplicated【复制一份新的按键布局】 2:搜索文件Windows copy.xml 3:将原来的Windows copy.xml 替换成我的按键布局 四:基础设置【便捷】 1:File---Settings---General---Change font size with Ctrl + Mouse Wheel 【打勾✔】 【Ctrl+鼠标滑轮可缩放大小】 2:File---Settings---Inspections---右边搜索Typo【取消勾勾】【或者是取消Spelling Error】 3:FIle---Settings---plguins---搜索Translation并下载【看个人需求】【这是翻译插件】 4:File---Settings---Editor---Font---Size设置26,字体我推荐JetBrains Mono 5:File---Settings---Appearance---自己看着设置,这里size我选择20。字体自选 6:设置背景 五:方法\函数模板设置【方便查看、看图】 #需要在【方法\函数】前加输入自己设置的模板快捷按键才会生成模板注释 二:点击  1:Abbreviation内我写的是/add代表我输入/add+Tab【在Expand With中设置】就会生成注释 2:这个符号$$里面的参数随便填写,包括前面的Name也是,填完后就可以选择Edit variables 3:填写完成后根据下图就可以翻译一下Expression内表达式的意思,比如date()就是日期然后Okay【不懂下载个百度翻译】  六:类模板设置#创建类文件后会自动生成模板注释  三:具体的模板的变量参数还得看官网给的文档、但是大部分都是相同的 ————————————————          原文链接:https://blog.csdn.net/qq_38431616/article/details/120272815 
  • [技术干货] JetBrains全家桶vmoptions配置文件(Ubuntu)
    Android Studio:  ~/.config/Google/AndroidStudioPreview2022.3/studio64.vmoptions  其他IDE:  ~/.config/JetBrains/RustRover2023.3/rustrover64.vmoptions ~/.config/JetBrains/PyCharm2023.3/pycharm64.vmoptions ~/.config/JetBrains/GoLand2023.1/goland64.vmoptions 以上是安装包默认的配置文件位置,如果设置以下环境变量,则会优先使用环境变量中指定的配置文件:  JETBRAINSCLIENT_VM_OPTIONS=/home/jetbra/vmoptions/jetbrainsclient.vmoptions GOLAND_VM_OPTIONS=/home/jetbra/vmoptions/goland.vmoptions WEBSTORM_VM_OPTIONS=/home/jetbra/vmoptions/webstorm.vmoptions PHPSTORM_VM_OPTIONS=/home/jetbra/vmoptions/phpstorm.vmoptions WEBIDE_VM_OPTIONS=/home/jetbra/vmoptions/webide.vmoptions GATEWAY_VM_OPTIONS=/home/jetbra/vmoptions/gateway.vmoptions DATASPELL_VM_OPTIONS=/home/jetbra/vmoptions/dataspell.vmoptions APPCODE_VM_OPTIONS=/home/jetbra/vmoptions/appcode.vmoptions IDEA_VM_OPTIONS=/home/jetbra/vmoptions/idea.vmoptions STUDIO_VM_OPTIONS=/home/jetbra/vmoptions/studio.vmoptions CLION_VM_OPTIONS=/home/jetbra/vmoptions/clion.vmoptions DATAGRIP_VM_OPTIONS=/home/jetbra/vmoptions/datagrip.vmoptions RIDER_VM_OPTIONS=/home/jetbra/vmoptions/rider.vmoptions JETBRAINS_CLIENT_VM_OPTIONS=/home/jetbra/vmoptions/jetbrains_client.vmoptions PYCHARM_VM_OPTIONS=/home/jetbra/vmoptions/pycharm.vmoptions RUBYMINE_VM_OPTIONS=/home/jetbra/vmoptions/rubymine.vmoptions DEVECOSTUDIO_VM_OPTIONS=/home/jetbra/vmoptions/devecostudio.vmoptions ———————————————— 原文链接:https://blog.csdn.net/yinminsumeng/article/details/137033690 
总条数:764 到第
上滑加载中