-
选择日志与监控系统的集成方案,是构建高效可观测性体系的关键一步。一个良好的集成不仅能提升故障排查效率,还能降低运维成本、避免数据孤岛。以下是系统化的选型思路和实践建议,适用于从初创团队到大型企业的不同场景。一、明确核心需求在选型前,先回答以下问题:问题决策影响业务规模:日均日志量多少?服务数量?决定是否需要分布式架构、冷热分层SLA 要求:能否容忍分钟级延迟?是否需实时告警?影响采集与查询引擎选型团队技能:是否有专职 SRE?熟悉哪些技术栈?决定自建 or 托管合规要求:数据是否必须私有化部署?排除公有云托管方案预算限制:愿意为可观测性投入多少成本?开源 vs 商业产品的权衡关键原则:不要为“未来可能的需求”过度设计,优先解决当前最痛的点(如“无法快速定位线上错误”)。二、集成架构的三种主流模式1. 统一平台模式(All-in-One)代表方案:Datadog、New Relic、阿里云 ARMS、腾讯云可观测平台特点:日志、指标、链路追踪一体化采集与展示自动关联 trace_id、service 等上下文内置智能告警、仪表盘、SLO 管理适用场景:团队规模小,希望快速上线愿意接受按量付费(通常按主机数或数据量计费)不想维护底层基础设施 优势:开箱即用,体验流畅 劣势:长期成本高,数据迁移困难2. 开源组合模式(Best-of-Breed)典型栈:采集:OpenTelemetry SDK + Fluent Bit / Vector传输:OpenTelemetry Collector存储与查询:Logs → Grafana Loki / ElasticsearchMetrics → Prometheus + Thanos / VictoriaMetricsTraces → Jaeger / Grafana Tempo可视化:Grafana(统一面板)适用场景:需要数据自主可控有运维能力,追求成本优化希望避免厂商锁定 优势:灵活、可扩展、长期成本低 劣势:需自行保障高可用、扩缩容、备份3. 混合模式(Hybrid)策略:核心系统自建(保障安全+成本),边缘或临时项目使用托管服务示例:生产环境:Loki + Prometheus + Tempo(私有部署)测试环境 / 新业务:Datadog(快速验证)优势:兼顾控制力与敏捷性三、关键技术考量点1. 上下文关联能力确保日志、指标、链路能通过共同字段(如 trace_id, service.name, pod)联动查询。推荐做法:应用使用 OpenTelemetry SDK,自动注入 trace_id 到日志日志格式为 JSON,包含标准字段(遵循 OTel Semantic Conventions)2. 采集性能与资源开销避免在应用进程内做 heavy parsing(如正则提取)使用轻量级 agent(如 Fluent Bit < Fluentd,Vector 性能更优)启用批处理和压缩(gzip)减少网络流量3. 查询语言与体验Prometheus 的 PromQL 适合指标分析Loki 的 LogQL 支持标签过滤 + 日志内容搜索Elasticsearch 的 Lucene 语法强大但学习成本高 尽量统一查询入口(如 Grafana),降低用户认知负担4. 告警闭环告警应基于指标(如错误率 > 1%),而非原始日志告警信息需包含上下文链接(如“点击查看相关 Trace”)支持自动静默、升级、通知渠道(企业微信/钉钉/Slack) 实施路线图Phase 1:基础覆盖应用接入 OTel SDK部署统一日志采集(Fluent Bit → Loki)配置核心服务指标监控(Prometheus)Phase 2:关联与告警实现日志 ↔ Trace 联动设置关键业务告警(如 API 错误率、延迟 P99)Phase 3:优化与治理引入采样、冷热分层建立日志规范(禁止随意打 INFO)接入 SLO 和用户体验监控(RUM) 日志与监控的集成,不是“选一个工具”,而是设计一套可持续演进的数据流水线。无论选择开源还是商业方案,核心目标始终是:让工程师在最短时间内,获得最多有效信息。建议从一个小而完整的闭环开始(如“一个服务的日志+指标+告警”),验证流程后再横向扩展。记住:最好的可观测性系统,是那个被团队真正用起来的系统,而不是功能最全的那个。
-
在复杂的分布式系统中,传统的监控手段——如简单的指标采集和日志查看——已难以满足快速定位问题、理解系统行为和保障用户体验的需求。可观测性(Observability)作为新一代系统洞察方法论,正逐步取代传统监控,成为云原生时代运维与开发的核心能力。本文将深入探讨可观测性的三大支柱(日志、指标、链路追踪)如何协同工作,分析主流技术栈的选型逻辑,并分享构建高效、低成本、可扩展可观测性系统的工程实践。什么是可观测性?可观测性源于控制理论,指通过系统外部输出(如日志、指标)推断其内部状态的能力。在软件工程中,它强调无需修改代码即可理解系统运行时行为,尤其适用于黑盒或灰盒系统。与传统监控的区别在于:监控:基于已知问题设置告警(“已知的未知”)可观测性:支持探索未知问题(“未知的未知”)例如,当用户投诉“页面加载慢”,可观测性系统应能帮助工程师快速下钻:是某个数据库查询变慢?CDN 回源异常?还是第三方 API 延迟升高?可观测性的三大支柱1. 日志(Logs):记录离散事件日志是最原始的系统输出,通常为带时间戳的文本或结构化 JSON。用途:调试、审计、错误追踪挑战:数据量大、非结构化、检索成本高2. 指标(Metrics):聚合的数值序列指标是对系统状态的量化描述,如 CPU 使用率、HTTP 请求数、错误率。特点:高压缩比、低存储成本、适合告警局限:丢失上下文,无法还原具体请求示例:http_requests_total{method="POST", status="500", path="/api/order"}3. 链路追踪(Traces):请求的全链路视图追踪记录单个请求在多个服务间的流转路径,由一系列 Span 组成。核心价值:可视化调用拓扑、定位性能瓶颈、识别异常服务标准协议:OpenTelemetry(OTLP)已成为事实标准,取代早期的 Zipkin、Jaeger 协议。技术栈选型:开源 vs 托管开源自建方案日志:Fluent Bit → Kafka → Loki / Elasticsearch指标:Prometheus + Thanos(长期存储)追踪:OpenTelemetry Collector → Jaeger / Tempo可视化:Grafana(统一面板)优势:成本可控、数据自主、灵活定制代价:运维复杂、需处理扩缩容、高可用保障云托管服务AWS:CloudWatch Logs + X-Ray + Managed PrometheusGCP:Cloud Logging + Cloud Trace + Cloud Monitoring第三方:Datadog、New Relic、Splunk Observability优势:开箱即用、自动扩缩、集成 AI 异常检测代价:按量计费成本高,存在厂商锁定风险选型建议:初创团队/成本敏感 → 开源组合(Loki + Prometheus + Tempo + Grafana)中大型企业/追求效率 → 托管服务 + OpenTelemetry 标准接入统一数据采集:OpenTelemetry 的核心作用OpenTelemetry(OTel)是 CNCF 主导的可观测性标准,提供:语言 SDK:自动注入 trace_id、采集指标(支持 Java、Go、Python 等)Collector:接收、处理、转发 Logs/Metrics/Traces语义约定:统一字段命名(如 http.method, db.statement)典型架构:App (OTel SDK) → OTel Collector → Backend (Loki/Prometheus/Jaeger)通过 OTel,应用只需一次埋点,即可将数据分发至多个后端,避免 vendor lock-in。成本优化策略可观测性数据增长迅猛,需主动控制成本:采样(Sampling)对高频 Trace 按比例采样(如 10%)错误请求 100% 采集,确保关键路径不丢失日志分级生产环境关闭 DEBUG 日志WARN/ERROR 日志强制保留冷热分层存储热数据(7天):高性能存储(Elasticsearch)冷数据(30天+):对象存储(S3)+ 查询引擎(Athena)指标预聚合在 Collector 层对高基数指标进行降维(如合并相似 label)构建一个轻量级可观测性平台假设你正在搭建一个微服务系统,预算有限但需基本可观测能力:采集层应用集成 OTel SDK,自动注入 trace_id 到日志部署 Fluent Bit DaemonSet 采集容器日志传输层使用 OTel Collector 接收数据,做简单过滤和批处理存储与查询Logs → Grafana Loki(低成本,标签索引)Metrics → Prometheus + VictoriaMetrics(高压缩比)Traces → Grafana Tempo(与 Loki 共享对象存储)可视化Grafana 统一面板:日志搜索、指标图表、Trace 下钻联动该方案总资源占用可控制在 2–4 核 CPU + 8GB 内存,适合中小规模集群。常见误区与避坑指南误区1:“日志越多越好”→ 后果:存储爆炸、检索变慢。应聚焦关键路径日志。误区2:“有了 Prometheus 就够了”→ 后果:无法定位具体错误请求。需结合 Trace 和 Logs。误区3:“可观测性只是运维的事”→ 正确做法:开发需在代码中合理埋点(如记录业务关键事件)。误区4:忽略上下文关联→ 必须确保 Logs、Metrics、Traces 共享相同标识(如 trace_id、service.name)。 可观测性不是一堆工具的堆砌,而是一种以问题为导向的系统思维。优秀的可观测性系统,能让工程师在 5 分钟内回答:“用户为什么失败?”、“系统哪里变慢了?”、“这次发布是否引入了风险?”。随着 OpenTelemetry 的普及和云原生生态的成熟,构建高效、统一、低成本的可观测性平台已不再是巨头专属。无论团队规模大小,只要坚持结构化数据、统一上下文、分层存储和成本意识,就能让系统真正“可见、可查、可信赖”。毕竟,在复杂的世界里,看得见,才安心。
-
1. 检索增强生成(RAG, Retrieval-Augmented Generation)RAG 是当前大语言模型(LLM)落地的关键架构之一,其核心思想是:在生成答案前,先从外部知识库中检索相关上下文,再将上下文与用户问题一同输入 LLM,从而提升回答的准确性、时效性和可解释性。倒排索引的作用:对知识库文档(如 FAQ、产品手册、维基页面)进行分词并构建倒排索引用户提问时,提取关键词(或经 query expansion 后),通过倒排索引快速召回包含这些关键词的候选文档作为第一阶段粗排(Candidate Generation),高效过滤出 Top-K 相关文档(如 100 篇),供后续精排或向量重排使用优势:检索速度快,适合处理海量文本可解释性强(能明确看到命中了哪些关键词)对拼写错误、同义词可通过 Analyzer(如 IK 分词器 + 同义词库)优化2. 混合检索(Hybrid Search):关键词 + 语义的双重保障纯向量检索虽能理解语义相似性,但对精确术语、实体名、代码片段、ID 等字面匹配需求表现不佳。而倒排索引恰好擅长此类任务。混合策略:两路召回融合:一路用倒排索引做关键词匹配,一路用向量索引做语义匹配,再加权合并结果(如 Reciprocal Rank Fusion)级联过滤:先用倒排索引召回包含关键实体(如“iPhone 15”、“北京”)的文档,再在子集中做向量精排元数据过滤:利用倒排索引对文档的标签、类别、时间等结构化字段进行高效过滤应用场景:电商搜索:“红色连衣裙 2024新款” → 倒排索引确保“红色”“连衣裙”“2024”被精确命中,向量模型理解“新款”语义技术文档搜索:“如何修复 Kubernetes Pod CrashLoopBackOff” → 倒排索引精准捕获错误码,向量模型理解“修复”意图3. 多模态检索中的元数据索引在图像、视频、音频等多模态 AI 系统中,原始内容被编码为向量,但其关联的文本元数据(如标题、标签、OCR 识别结果、ASR 转录文本)仍需高效检索。倒排索引的角色:对图像的 OCR 文本、视频字幕、音频转写内容构建倒排索引用户输入文本查询时,可同时触发:向量路径:将 query 编码为向量,搜索视觉/听觉特征向量文本路径:通过倒排索引搜索元数据中的关键词二者结果融合,提升召回率案例:视频平台搜索“包含‘特斯拉发布会’字幕的视频” → 倒排索引快速定位字幕含该短语的视频片段医疗影像系统:通过报告中的“肺结节”“CT”等关键词召回相关病例,再结合影像向量比对4. 负样本挖掘与训练数据构建在训练对比学习(Contrastive Learning)或双塔模型时,需要大量难负样本(Hard Negative) 来提升模型判别能力。倒排索引辅助负采样:给定一个正样本 query,用倒排索引召回一批“看似相关但实际无关”的文档(如包含部分关键词但主题偏离)这些文档作为高质量负样本,比随机负样本更能提升模型性能5. 实时更新与高吞吐写入场景相比向量索引(如 FAISS、HNSW)通常需要批量重建或复杂增量更新,倒排索引天然支持高频写入和实时可见(如 Elasticsearch 的近实时 refresh)。AI 应用价值:用户行为日志、实时新闻、社交媒体内容等动态数据,可立即被倒排索引收录并参与检索在 RAG 系统中,新上传的文档几秒内即可被查询到,满足企业对“知识即时生效”的需求6. 可解释性与调试支持AI 系统常被视为“黑盒”,而倒排索引提供了透明的检索依据:可展示“因命中关键词‘退款政策’而推荐该文档”便于人工审核、规则干预(如屏蔽某些关键词结果)在合规场景(如金融、医疗)中满足审计要求
-
在数据驱动的时代,数据库作为信息系统的核心组件,其性能直接决定了应用的响应速度与用户体验。而索引(Index),作为加速数据检索的关键技术,堪称数据库的“高速公路”。不同的数据访问模式催生了多样化的索引结构——从关系型数据库广泛采用的 B+ 树,到 NoSQL 系统偏爱的 LSM 树(Log-Structured Merge-Tree),再到搜索引擎依赖的 倒排索引(Inverted Index)。理解这些索引的设计哲学、适用场景与实现细节,是构建高性能数据系统的基础。一、B+ 树:OLTP 场景的黄金标准1. 结构特点B+ 树是一种多路平衡查找树,专为磁盘 I/O 优化设计:所有数据存储在叶子节点,非叶子节点仅保存索引键和指针叶子节点通过链表串联,支持高效的范围查询节点大小通常设为一页(如 4KB 或 16KB),减少磁盘随机读取次数例如,在 MySQL InnoDB 中,主键索引即为聚簇 B+ 树,行数据直接存储在叶子节点;二级索引则为非聚簇 B+ 树,叶子节点存储主键值。2. 优势与适用场景点查高效:时间复杂度 O(logₙN),n 为分支因子(通常数百)范围扫描快:叶子节点链表顺序遍历,避免回溯写入稳定:每次更新仅修改少量页面,支持 WAL(Write-Ahead Logging)保证事务原子性3. 写放大与碎片问题频繁的随机插入可能导致页分裂,产生空间碎片;删除操作则留下“空洞”,需定期 OPTIMIZE TABLE 回收空间。二、LSM 树:为写密集型负载而生当写入吞吐成为瓶颈(如日志、时序数据、消息队列),B+ 树的随机写开销显得力不从心。LSM 树通过将随机写转化为顺序写,极大提升写入性能。1. 核心思想:分层合并LSM 树将数据分为多层(Level),典型结构如下:MemTable:内存中的有序结构(如 SkipList),接收新写入Immutable MemTable:写满后冻结,异步刷入磁盘SSTable(Sorted String Table):磁盘上的只读有序文件Compaction:后台合并小 SSTable 为大文件,清除过期/删除数据写入流程:数据先写入 WAL(保证持久性)插入 MemTable(内存操作,极快)MemTable 满后转为 Immutable,刷盘生成 Level 0 SSTable后台线程定期执行 Compaction,合并多层 SSTable2. 优势与代价写入吞吐极高:顺序写磁盘,接近硬件极限空间放大:Compaction 过程中临时占用额外空间读放大:需查询多层 SSTable 和 MemTable,可能触发多次 I/O写放大:同一数据在 Compaction 中被反复重写3. 优化策略布隆过滤器(Bloom Filter):快速判断 key 是否存在于某 SSTable,减少无效 I/O分层 Compaction(Leveled) vs 大小分级(Size-Tiered):权衡读写放大与空间效率三、倒排索引:全文搜索的基石在搜索引擎或需要文本匹配的场景中,传统索引无法高效支持“关键词 → 文档”的反向查找。倒排索引应运而生。1. 结构组成倒排索引由两部分构成:词典(Dictionary):所有唯一词条的有序列表,通常用 B+ 树或 FST(Finite State Transducer)存储倒排列表(Posting List):每个词条对应的文档 ID 列表,常附带位置、频率等元数据例如,对两篇文档建立索引:Doc1: "The quick brown fox"Doc2: "Jumped over the lazy dog"倒排列表:"the" → [Doc1(pos=0), Doc2(pos=2)]"fox" → [Doc1(pos=3)]...2. 高效压缩与跳表优化Delta 编码:文档 ID 通常递增,存储相邻差值(如 [100, 105, 110] → [100, 5, 5])跳表(Skip List):在长 Posting List 中设置跳跃指针,加速“AND/OR”查询的交并集计算3. 实时更新挑战传统倒排索引为静态构建。为支持实时写入,现代引擎(如 Elasticsearch)采用:分段(Segment)机制:新数据写入新 Segment,后台合并近实时(NRT)搜索:通过 refresh 操作使新数据可查(默认 1 秒)四、三大索引对比与选型指南特性B+ 树LSM 树倒排索引核心优势点查 & 范围查询快写入吞吐高文本关键词检索高效写入性能中等(随机写)极高(顺序写)中等(需分段合并)读取性能稳定低延迟可能较高(多层查找)依赖压缩与缓存事务支持强(MVCC + WAL)弱(通常最终一致)无典型系统MySQL, PostgreSQLCassandra, RocksDBElasticsearch, Lucene适用场景OLTP、高一致性业务日志、时序、IoT搜索、文本分析五、混合索引:现代数据库的融合趋势单一索引难以满足复杂需求,新一代数据库开始融合多种结构:TiDB:底层使用 LSM 树(RocksDB),上层构建分布式 B+ 树索引MongoDB:默认 B 树索引,同时支持全文检索(倒排)和地理空间索引ClickHouse:主键使用稀疏索引(类似 B+ 树),配合 MergeTree 引擎优化分析查询此外,向量索引(如 HNSW、IVF)正随着 AI 应用兴起,用于相似性搜索,进一步拓展索引的边界。六、结语索引是数据库性能的“命门”,其选择绝非技术炫技,而是对业务访问模式的深刻理解。B+ 树在事务型系统中屹立不倒,LSM 树在写密集场景大放异彩,倒排索引则撑起了整个搜索生态。作为工程师,我们不仅要知其然,更要知其所以然——理解每种索引背后的权衡(Trade-off):读写放大、空间效率、一致性模型。唯有如此,才能在面对海量数据、高并发、低延迟的挑战时,做出最合理的架构决策。未来,随着硬件演进(如持久内存、NVMe)与 AI 负载增长,索引技术将继续演化,但其核心使命始终不变:让数据触手可及。
-
全网同名「悬着的心终于死了」,全网同名认证~ 题主要批量修改网上下来的一些图片的后缀名,因为之前学艺不精,搞出来很多问题,这里记录一下(>_<)。之前学习操作文件的时候很草率,总结了一套文件基本操作流程:现在回来才发现这个套路并不适合所有的文件类型先上错误代码:import java.io.*;public class Main { public static void main(String[] args) throws Exception { String imgPath="D:\\文件夹/00015.webp"; File file=new File(imgPath); FileReader fileReader=new FileReader(file); FileWriter fileWriter=new FileWriter(file); String fatherPath=file.getParent(); String name=file.getName(); System.out.println(file.renameTo(new File(fatherPath,toJpg(name)))); try { fileReader.close(); fileWriter.close(); }catch (Exception e){ e.printStackTrace(); } } public static String toJpg(String oName){ int path=oName.lastIndexOf("."); return oName.substring(0,path)+".jpg"; }}操作的时候,各种错误:这里我总结为2个问题:1.FileWriter错误使用就像前面所说,我是直接套“模板”写,FileReader和FileWriter虽然没用到,但没报错就没删(这次长记性了>m<)经过排查,发现因为我多写这一句FileWriter才导致图片损坏这里是搜集到的结果如果 WebP 文件已经损坏,可以考虑使用专门的工具进行修复。例如,FabConvert 提供了一个免费的 WEBP 修复工具,可以在任何具有现代网络浏览器的系统上运行,并且没有使用限制未正确关闭流:在使用 FileWriter 写入数据后,如果没有正确关闭流,可能会导致数据未完全写入,从而损坏文件 。异常处理不当:如果在写入过程中发生异常,而异常没有被正确捕获和处理,可能会导致文件处于不一致的状态 。写入中途断电或系统崩溃:在写入过程中,如果遇到断电或系统崩溃等意外情况,可能会导致文件写入未完成,从而损坏文件。文件系统限制:某些文件系统可能有写入限制,例如最大文件大小或特定格式要求,不遵守这些限制可能会导致文件损坏。缓存问题:有时候浏览器可能会缓存旧版本的图片文件,导致新的 WebP 图片无法加载,这可能是文件损坏的一个表现 。文件损坏:检查 WebP 图片文件是否损坏或完整。有时候图片文件可能会在上传或保存过程中出现问题 。为了避免文件损坏,应该采取以下措施:确保在使用 FileWriter 后正确关闭它。使用异常处理来捕获并处理写入过程中可能发生的错误。避免在没有适当同步机制的情况下进行并发写入。使用事务或日志记录来确保写入操作的原子性和一致性。检查磁盘空间,并确保应用程序有足够的权限来写入文件。这里我并没有用FileWriter写入数据,所以应该是文件系统限制的原因,希望有懂的大佬可以在评论区解答一下(ˊ˘ˋ*)♡。2.FileReader错误使用这里通过反复测试发现,在没有FileReader这一句是file.renameTo()是可以执行的,原因是在使用FileReader时会资源锁定。以下为总结:在许多编程语言中,当你使用 FileReader 或类似的文件读取类与一个 File 对象关联后,你通常不能再直接操作这个 File 对象来读取文件。这主要是因为以下几个原因:资源锁定:一旦 FileReader 打开了一个文件,操作系统可能会锁定这个文件,防止其他进程或线程同时读取或写入,以避免数据损坏或冲突。状态管理:FileReader 可能内部维护了文件的状态信息,如当前读取位置。如果尝试用同一个 File 对象再次创建 FileReader,可能会遇到状态不一致的问题。设计模式:编程语言的设计可能鼓励使用流式操作,即一次只通过一个流(如 FileReader)来处理文件,而不是同时打开多个流。资源释放:如果 FileReader 没有被正确关闭,它可能会持续占用文件资源,导致其他操作无法进行。API限制:某些编程语言或库可能在API设计上限制了对同一个文件对象的多次使用,以简化资源管理和错误处理。如果需要在同一个程序中多次读取同一个文件,通常的做法是:在每次读取操作之后,确保关闭 FileReader 对象。如果需要再次读取,可以重新打开文件,创建一个新的 FileReader 对象。例如,在Java中,你可以这样做:File file = new File("path/to/your/file.txt");// 第一次读取FileReader reader1 = new FileReader(file);// ... 执行读取操作 ...reader1.close();// 第二次读取FileReader reader2 = new FileReader(file);// ... 执行读取操作 ...reader2.close();综上,修改了这两个问题后,修改文件后缀名就成功了import java.io.*;public class Main { public static void main(String[] args) throws Exception { String imgPath="D:\\文件夹/00015.webp"; File file=new File(imgPath); //FileReader fileReader=new FileReader(file); //FileWriter fileWriter=new FileWriter(file); String fatherPath=file.getParent(); String name=file.getName(); System.out.println(file.renameTo(new File(fatherPath,toJpg(name)))); } public static String toJpg(String oName){ int path=oName.lastIndexOf("."); return oName.substring(0,path)+".jpg"; }最后叨叨一句,自己准备的模板一定要完全弄懂口牙_(:зゝ∠)_
-
JFR(Java Flight Recorder)是 JDK 内置的高性能诊断工具,以极低开销记录 JVM 和应用运行时的关键事件。然而,不当配置可能导致录制开销升高、文件过大或分析困难。尤其在高并发、长时间运行的生产环境(如鲲鹏 ARM64 服务器)中,合理优化 JFR 的使用策略至关重要。本文将从 录制配置、资源控制、事件筛选、分析效率 四个维度,系统讲解如何优化 JFR 的性能表现。一、核心原则:平衡“数据完整性”与“运行开销”JFR 的默认配置(profile.jfc)已针对通用场景做了权衡,但实际业务需根据目标调整:目标推荐策略长期监控(7×24)仅启用关键事件(GC、线程、CPU 采样),降低采样频率故障复现启用详细事件(方法、异常、I/O),短时间高保真录制性能压测对比使用统一配置,确保数据可比性二、优化录制阶段的性能1. 选择合适的预设模板JDK 提供两个内置模板:default.jfc:基础事件(低开销,适合长期运行)profile.jfc:包含方法采样、锁竞争等(中等开销,适合性能分析)建议:# 长期监控用 default-XX:StartFlightRecording=settings=default,duration=1h,filename=/data/jfr/low.jfr# 深度分析用 profile-XX:StartFlightRecording=settings=profile,duration=5m,filename=/data/jfr/high.jfr 毕昇 JDK 还提供 kunpeng-optimized.jfc(如有),可进一步适配 ARM64。2. 自定义 .jfc 配置文件(推荐)复制并修改模板,关闭非必要事件:<!-- custom-low-overhead.jfc --><configuration ...> <event name="jdk.MethodSampling"> <setting name="enabled">false</setting> <!-- 关闭方法采样 --> </event> <event name="jdk.JavaMonitorEnter"> <setting name="enabled">true</setting> <setting name="threshold">10ms</setting> <!-- 仅记录 >10ms 的锁等待 --> </event> <event name="jdk.GCPhasePause"> <setting name="enabled">true</setting> </event></configuration>启动时指定:-XX:StartFlightRecording=settings=/path/to/custom-low-overhead.jfc,...3. 控制录制时长与文件大小避免无限制录制导致磁盘爆满:# 方式1:固定时长duration=10m# 方式2:循环录制(保留最近数据)maxsize=500MB,maxage=1h# 方式3:条件触发(JDK 17+ 支持)-XX:FlightRecorderOptions:repository=/tmp/jfr-cache生产建议:单文件 ≤ 1GB总录制时长 ≤ 30 分钟(除非明确需要长期趋势)4. 调整采样频率(降低 CPU 开销)关键参数:jdk.ThreadCPULoad:线程 CPU 采样间隔(默认 1s)jdk.ExecutionSample:方法栈采样间隔(默认 10ms)在自定义 .jfc 中调整:<event name="jdk.ExecutionSample"> <setting name="period">100ms</setting> <!-- 从 10ms 放宽到 100ms --></event> 注意:采样间隔越长,热点方法识别精度越低。三、减少 I/O 与内存开销1. 使用高速存储路径将 .jfr 文件写入 SSD 或内存盘:filename=/dev/shm/app.jfr # 写入 tmpfs(内存文件系统) 优势:避免磁盘 I/O 成为瓶颈 风险:重启丢失,需及时备份2. 启用压缩(JDK 17+)-XX:FlightRecorderOptions=compress=true可减少 30%~50% 文件体积。3. 避免多进程同时写同一目录每个 Java 进程应使用独立子目录,防止文件锁竞争。四、优化分析阶段的效率1. 使用命令行快速筛查(避免 GUI 开销)# 查看 GC 暂停总时间jfr print --events GCPhasePause app.jfr | grep "duration"# 统计最耗时的 10 个方法jfr summary app.jfr --category "Code" | head -n 102. 在分析机而非生产机运行 JMC将 .jfr 文件拷贝至开发机或专用分析服务器;避免在生产环境启动图形界面工具。3. 使用脚本自动化分析结合 jfr 命令 + Shell/Python 脚本,实现:自动提取关键指标生成性能报告触发告警(如 GC 暂停 > 100ms)示例脚本片段:MAX_PAUSE=$(jfr print --events GCPhasePause app.jfr | awk '/duration/ {print $2}' | sort -nr | head -1)if [ "$MAX_PAUSE" -gt 100000000 ]; then # 100ms in nanoseconds echo "ALERT: Max GC pause exceeds 100ms!"fi五、鲲鹏 ARM64 环境下的特别建议优先使用毕昇 JDK其 JFR 实现针对鲲鹏处理器的缓存、NUMA 架构优化,事件采集效率更高。关注 ARM64 特有事件如:jdk.CPULoad:ARM64 大小核调度可能影响 CPU 利用率jdk.NativeLibrary:验证 native 库是否为 ARM64 编译对比 x86 基线在相同负载下,分别录制 x86 与鲲鹏的 JFR 数据,使用 JMC 的 Compare 功能 定位架构差异点。
-
什么是 JFR?Java Flight Recorder(JFR)是 Oracle 自 JDK 7 引入、并在 JDK 11+ 开源的高性能事件记录框架。它以极低的性能开销(通常 <1%)持续收集 JVM 和应用层的关键事件,包括:GC 活动(暂停时间、堆变化)线程状态(阻塞、锁竞争)方法采样(CPU 热点)I/O 操作异常抛出类加载JIT 编译行为优势:内置于 JDK,无需额外依赖支持长时间录制(小时级)事件粒度细、上下文完整官方工具 JMC(Java Mission Control)提供可视化分析启用 JFR 并录制数据方法 1:启动时开启录制(推荐用于生产)java -XX:+FlightRecorder \ -XX:StartFlightRecording=duration=300s,filename=/tmp/app.jfr,name=MyAppProfile \ -jar your-app.jar参数说明:duration=300s:录制 5 分钟后自动停止filename:输出文件路径name:录制会话名称方法 2:运行时动态开启(需 JDK 14+ 或使用 jcmd)# 查看 Java 进程 PIDjps# 启动 60 秒录制jcmd <PID> JFR.start duration=60s filename=/tmp/runtime.jfr# 手动停止(可选)jcmd <PID> JFR.stop name=1注意:鲲鹏服务器上建议将 .jfr 文件保存至高速 SSD,避免 I/O 影响录制性能。 分析 JFR 数据工具 1:Java Mission Control(JMC)下载 JMC(支持 ARM64):华为毕昇 JDK 自带 JMC或从 Adoptium 下载 ARM64 版打开 .jfr 文件:jmc /tmp/app.jfr关键分析视图:Memory → Garbage Collections:GC 频率与暂停时间Threads → Thread Dump:线程阻塞与锁竞争Code → Hot Methods:CPU 消耗最高的方法I/O → File Read/Write:磁盘 I/O 瓶颈工具 2:命令行快速查看(无需 GUI)# 使用 jfr 工具(JDK 19+ 内置)jfr print --events GCPhasePause /tmp/app.jfr# 统计方法采样jfr summary /tmp/app.jfr 在鲲鹏服务器上,JFR 是 Java 应用性能调优不可或缺的工具。结合毕昇 JDK 的 ARM64 优化,可以:精准识别 GC、锁、I/O 等瓶颈;对比 x86 与 ARM64 的性能差异;验证 JVM 参数调优效果;构建性能基线,支撑容量规划。
-
在 Spring Boot 开发中,数组(Array)作为一种基础而高效的数据结构,广泛应用于参数接收、配置管理、批量处理等场景。尽管 Java 生态中更常使用 List、Set 等集合类型,但在特定场合(如性能敏感、固定长度、与前端/接口协议对齐等),原生数组仍有不可替代的价值。一、控制器中接收数组参数1.1 接收 URL 查询参数中的数组前端可通过重复参数名传递数组,Spring Boot 自动绑定为 Java 数组或 List。@RestControllerpublic class UserController { // 方式1:使用数组接收 @GetMapping("/users") public String getUsersByIds(@RequestParam("ids") Long[] ids) { return "Received IDs: " + Arrays.toString(ids); } // 方式2:使用 List 接收(推荐) @GetMapping("/users/list") public String getUsersByIdsList(@RequestParam("ids") List<Long> ids) { return "Received IDs: " + ids; }}请求示例:GET /users?ids=1&ids=2&ids=3 支持:Long[]、String[]、int[] 等基本类型及包装类数组 注意:若参数为空(如 /users),ids 将为 null,建议加 required = false 或设默认值@RequestParam(value = "ids", required = false, defaultValue = "") String[] ids1.2 接收 JSON 请求体中的数组当请求体为 JSON 格式时,可直接映射到对象字段中的数组。public class BatchUpdateRequest { private Long[] userIds; // 或 List<Long> userIds private String status; // getter/setter}@PostMapping("/batch-update")public ResponseEntity<String> batchUpdate(@RequestBody BatchUpdateRequest request) { System.out.println("User IDs: " + Arrays.toString(request.getUserIds())); return ResponseEntity.ok("Success");}请求体示例:{ "userIds": [101, 102, 103], "status": "ACTIVE"}Jackson 默认支持数组反序列化若需自定义行为,可配置 ObjectMapper二、在配置文件中使用数组2.1 application.yml 中定义数组app: supported-languages: [zh-CN, en-US, ja-JP] max-retry-attempts: 3 admin-ids: - 1001 - 1002 - 10032.2 使用 @ConfigurationProperties 绑定@Component@ConfigurationProperties(prefix = "app")@Data // Lombok 注解,自动生成 getter/setterpublic class AppProperties { private String[] supportedLanguages; private int maxRetryAttempts; private Long[] adminIds;} 支持 String[]、int[]、Long[] 等 不支持泛型数组(如 List<String> 需用 List)2.3 使用 @Value 注入数组(不推荐复杂场景)@Value("${app.supported-languages}")private String[] supportedLanguages;// 或使用 SpEL 表达式(适用于简单字符串数组)@Value("#{'${app.admin-ids}'.split(',')}")private Long[] adminIds; // 需确保配置为逗号分隔 缺点:类型转换弱、错误提示不友好,建议优先使用 @ConfigurationProperties三、数组与 JSON 序列化Spring Boot 默认使用 Jackson 处理 JSON。3.1 返回数组作为响应体@GetMapping("/languages")public String[] getSupportedLanguages() { return new String[]{"zh-CN", "en-US", "ja-JP"};}响应结果:["zh-CN","en-US","ja-JP"]3.2 自定义序列化行为(可选)若需控制空数组、null 值等行为,可在字段或类上添加注解:public class ApiResponse { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String[] tags = {}; // 空数组不返回 // getter/setter}
-
一、基础概念进程 我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。线程 线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。并发性和并行的区别 并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。二、创建线程的三种方式2.1 通过继承Thread类来启用Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。Thread类的特性 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。流程:定义子类继承Thread类;子类中重写Thread类中的run方法;创建Thread子类对象,即创建了线程对象;调用线程对象start方法:启动线程,调用run方法。 具体代码示例首先构建一个继承Thread类的子类//继承Thread类的方式实现多线程public class TestThread extends Thread{ @Override public void run(){ System.out.println("多线程运行的代码"); }}AI写代码java运行 调用线程public class Test{ public static void main(String[]args){ Thread t = new TestThread(); t.start(); //启动线程 }}AI写代码java运行2.2 实现Runnable接口来实现流程定义子类,实现Runnable接口。子类中重写Runnable接口中的run方法。通过Thread类含参构造器创建线程对象。将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。 实现Runnable接口public class TestRunnable implements Runnable{ @Override public void run(){ System.out.println("实现Runnable接口运行多线程"); }}AI写代码java运行实现多线程public class Test{ public static void main(String[]args){ Thread t = new Thread(new TestRunnable); //带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取 //Thread t = new Thread(new TestRunnable,"the FirstThread"); t.start(); //启动线程 }}AI写代码java运行与继承Thread类的区别继承Thread:线程代码存放Thread子类run方法中。重写run方法实现Runnable:线程代码存在接口的子类的run方法。实现run方法 实现Runnable接口方法的好处 实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 2.3 实现Callable接口 在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。public class Test{ public static void main(String[]args){ //创建callable对象 ThirdThread tt = new ThirdThread(); //使用FutureTask来包装Callable对象 FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{ ... ... }); new Thread(task,"有返回值的线程").start(); try{ //获取线程返回值 System.out.println("子线程的返回值" + task.get()); }catch (EXception ex){ ex.printStackTrace(); } }}AI写代码java运行Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回! 三、Thread类的相关方法常用方法如下:void start():启动线程,并执行对象的run(0方法run():线程在被调度时执行的操作String getName():返回线程的名称void setName(String name):设置该线程名称static currentThread():返回当前线程 public class Test{ public static void main(String[]args){ TestRun r1 = new TestRun(); Thread t1 = new Thread(r1); //为线程设置名称 t1.setName("线程t1"); t1.start(); //启动线程 System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0.... }}public class TestRun implements Runnable{ @Override public void run(){ System.out.println("实现Runnable接口运行多线程"); }}AI写代码java运行线程优先级线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()//获取优先级t1.getPriority();//设置优先级t1.setPriority(10);AI写代码java运行线程让步static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。Thread.yield();AI写代码java运行线程阻塞join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。try{ //获取线程返回值 t1.join();}catch (EXception ex){ ex.printStackTrace(); }AI写代码java运行线程睡眠try{ Thread.sleep(1000);//当前线程睡眠1000毫秒}catch(InterruptedException e)( e.printStackTrace();}AI写代码java运行线程生命结束t1.stop();AI写代码java运行判断当前线程是否存活t1.isAlive();AI写代码java运行四、生命周期线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;死亡:线程完成了它的全部工作或线程被提前强制性地中止 。 线程可能以如下三种方法结束:run或call方法执行完成后线程抛出一个未捕获的Exception或Error直接调用了stop()方法五、同步锁和死锁5.1 同步锁 多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo5.1.1 synchronized加锁的两种方式 synchronized同步锁关键字修饰 //使用synchronized同步锁关键字修饰需要同步执行的方法体public synchronized void drawing(int money){ 需要同步执行的代码}AI写代码java运行注意: 在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁!synchronized锁一段demo使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁 demo...}AI写代码java运行5.1.2 Lock 相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。ReentrantLock 实现demo class x{ //定义锁对象 private final ReentrantLock lock = new ReentrantLock(); //... //定义需要保证线程安全的方法 public void m(){ lock.lock(); try{ //需要保证线程安全的demo } finally{ lock.unlock(); } }}AI写代码java运行5.2 死锁不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。解决方法专门的算法、原则,比如加锁顺序一致尽量减少同步资源的定义,尽量避免锁未释放的场景六、线程通信 当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。6.1 传统的线程通信Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字。wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。notifyAll():唤醒等待的所有线程。//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可wait();notify();notifyAll();AI写代码6.2 使用Condition来控制线程通信 对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。signal():唤醒单个线程。signalAll():唤醒多个线程。//显示定义Lock对象Lock lock = new ReentrantLock();//获取ConditionCondition cond = lock.newCondition();//需要同步的方法中加锁public void fun(){ //加锁过程 lock.lock(); try{ if(条件) cond.await(); //线程进入等待 else{ //唤醒其他线程 cond.signalAll(); } }catch(InterruptedException e){ e.printStrackTrace(); }finally{ //锁的释放 lock.unlock(); }}AI写代码java运行6.3 使用阻塞队列来控制线程通信 除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。 在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。 七、线程池 系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。 ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。7.1 ExecutorService类使用示例使用线程池的步骤如下:调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;//开启6个线程的线程池ExecutorService pool = Executors.newFixedThreadPool(6);//创建Runnable实现类Runnable target = ()->{...} //提交线程任务到线程池pool.submit(); //关闭线程pool.shutdown();AI写代码java运行 用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。7.2 Java8中的ForkJoinPool 计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。构造器的两种方法ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池实现通用池的两个静态方法ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。int getCommonPoolParallelism():该方法返回通用池的并行级别注意: ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。class PrintTask extends RecursiveAction{ ... @Override protected void compute(){ ...... //分割任务 PrintTask t1 = new PrintTask(start,middle); PrintTask t2 = new PrintTask(middle,end); //并行执行子任务 t1.fork(); t2.fork(); }} public class Test{ public static void main(String[]args) throws Exception{ //实例化通用池对象 ForkJoinPool pool = new ForkJoinPool(); pool.submit(new PrintTask(0,1000)); //线程等待完成 pool.awaitTermination(2,TimeUnit.SECONDS); //关闭线程池 pool.shutdown(); }}AI写代码java运行总结 现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~————————————————版权声明:本文为CSDN博主「荔枝当大佬」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_62706049/article/details/131848628
-
篇讲了Lock锁、AQS相关的内容,本篇讲一下线程安全的类,拿来即用无需其他操作就能达到线程安全的效果,省力又省心 ~ ~你是否曾为多线程编程中的各种坑而头疼?本文将用生动比喻和实用代码,带你轻松掌握Java并发容器的精髓,让你的多线程程序既安全又高效!引言:为什么我们需要并发容器?想象一下传统的超市结账场景:只有一个收银台,所有人排成一队,效率低下。这就是传统集合在多线程环境下的写照。而现代并发容器就像拥有多个收银台的智能超市:多个收银台同时工作智能分配顾客到不同队列收银员之间互相协助在Java并发世界中,我们有三大法宝:ConcurrentHashMap - 智能分区的储物柜系统ConcurrentLinkedQueue - 无锁的快速通道阻塞队列 - 有协调员的等待区Fork/Join框架 - 团队协作的工作模式让我们一一探索它们的魔力!1. ConcurrentHashMap:智能分区的储物柜系统1.1 传统Map的问题:独木桥的困境// 传统HashMap在多线程环境下就像独木桥public class HashMapProblem { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // 多个线程同时操作HashMap,就像多人同时过独木桥 // 结果:有人掉水里(数据丢失),桥塌了(死循环) }}1.2 ConcurrentHashMap的解决方案:多车道高速公路分段锁设计:把整个Map分成多个小区域,每个区域独立加锁ConcurrentHashMap架构: ├── 区域1 (锁1) → 储物柜组1 ├── 区域2 (锁2) → 储物柜组2 ├── 区域3 (锁3) → 储物柜组3 └── ...核心优势:写操作只锁住对应的区域,其他区域仍可读写读操作基本不需要加锁大大提高了并发性能1.3 实战示例:高性能缓存系统/** * 基于ConcurrentHashMap的高性能缓存 * 像智能储物柜系统,支持高并发存取 */public class HighPerformanceCache<K, V> { private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>(); // 获取或计算缓存值(线程安全且高效) public V getOrCompute(K key, Supplier<V> supplier) { return cache.computeIfAbsent(key, k -> new CacheEntry<>(supplier.get())).getValue(); } // 批量获取,利用并发特性 public Map<K, V> getAll(Set<K> keys) { Map<K, V> result = new HashMap<>(); keys.forEach(key -> { CacheEntry<V> entry = cache.get(key); if (entry != null && !entry.isExpired()) { result.put(key, entry.getValue()); } }); return result; }}2. ConcurrentLinkedQueue:无锁的快速通道2.1 无锁队列的魔法传统队列就像只有一个入口的隧道,所有车辆必须排队。而ConcurrentLinkedQueue就像多入口的立体交通枢纽:// 无锁队列的生动理解public class LockFreeQueueAnalogy { public void trafficHubComparison() { // 传统阻塞队列:单入口隧道,经常堵车 // ConcurrentLinkedQueue:立体交通枢纽,多入口同时通行 // 秘密武器:CAS(Compare-And-Swap)算法 }}2.2 CAS:优雅的竞争解决CAS就像礼貌的询问:public class PoliteInquiry { public void casAnalogy() { // 传统加锁:像抢座位,谁先坐到就是谁的 // CAS无锁:像礼貌询问"这个座位有人吗?" // 如果没人就坐下,有人就找下一个座位 }}2.3 实战示例:高并发任务处理器/** * 基于ConcurrentLinkedQueue的高性能任务处理器 * 像高效的快递分拣中心 */public class HighPerformanceTaskProcessor { private final ConcurrentLinkedQueue<Runnable> taskQueue = new ConcurrentLinkedQueue<>(); // 提交任务 - 无锁操作,极高吞吐量 public void submit(Runnable task) { taskQueue.offer(task); // 像快递放入分拣流水线 startWorkerIfNeeded(); } // 工作线程 - 无锁获取任务 private class Worker implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { Runnable task = taskQueue.poll(); // 像从流水线取快递 if (task != null) { task.run(); // 处理任务 } } } }}3. 阻塞队列:有协调员的等待区3.1 阻塞队列的四种行为模式想象餐厅的四种接待方式:public class RestaurantReception { public void fourBehaviors() { // 1. 抛出异常 - 霸道的服务员 // "没位置了!走开!" // 2. 返回特殊值 - 礼貌的前台 // "抱歉现在没位置,您要不等会儿?" // 3. 一直阻塞 - 耐心的门童 // "请您在这稍等,有位置我马上叫您" // 4. 超时退出 - 体贴的经理 // "请您等待10分钟,如果还没位置我帮您安排其他餐厅" }}3.2 七种阻塞队列:不同的餐厅风格Java提供了7种阻塞队列,每种都有独特的"经营理念":ArrayBlockingQueue:传统固定座位餐厅// 有10个桌位的餐厅,公平模式ArrayBlockingQueue<String> restaurant = new ArrayBlockingQueue<>(10, true);LinkedBlockingQueue:可扩展的连锁餐厅// 最大容纳1000人的餐厅LinkedBlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>(1000);PriorityBlockingQueue:VIP贵宾厅// 按客户等级服务的贵宾厅PriorityBlockingQueue<Customer> vipLounge = new PriorityBlockingQueue<>();DelayQueue:延时电影院// 电影到点才能入场DelayQueue<MovieScreening> schedule = new DelayQueue<>();SynchronousQueue:一对一传球游戏// 不存储元素,每个put必须等待一个takeSynchronousQueue<String> ballChannel = new SynchronousQueue<>(true);3.3 实战示例:生产者-消费者模式/** * 生产者-消费者模式的完美实现 * 像工厂的装配流水线 */public class ProducerConsumerPattern { private final BlockingQueue<Item> assemblyLine; public ProducerConsumerPattern(int lineCapacity) { this.assemblyLine = new ArrayBlockingQueue<>(lineCapacity); } // 生产者:原材料入库 public void startProducers(int count) { for (int i = 0; i < count; i++) { new Thread(() -> { while (true) { Item item = produceItem(); assemblyLine.put(item); // 流水线满时等待 } }).start(); } } // 消费者:产品出库 public void startConsumers(int count) { for (int i = 0; i < count; i++) { new Thread(() -> { while (true) { Item item = assemblyLine.take(); // 流水线空时等待 consumeItem(item); } }).start(); } }}4. Fork/Join框架:团队协作的智慧4.1 分而治之的哲学Fork/Join框架的核心理念:大事化小,小事并行,结果汇总就像编写一本巨著:传统方式:一个人从头写到尾Fork/Join方式:分给多个作者同时写不同章节,最后汇总4.2 工作窃取算法:聪明的互助团队public class TeamWorkExample { public void workStealingInAction() { // 初始:4个工人,每人25个任务 // 工人A先完成自己的任务 // 工人B还有10个任务没完成 // 工作窃取:工人A从工人B的任务列表"偷"任务帮忙 // 结果:整体效率最大化,没有人闲着 }}4.3 实战示例:并行数组求和/** * 使用Fork/Join并行计算数组和 * 像团队协作完成大项目 */public class ParallelArraySum { static class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 1000; // 阈值 private final long[] array; private final int start, end; public SumTask(long[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { // 如果任务足够小,直接计算 if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) sum += array[i]; return sum; } // 拆分成两个子任务 int mid = (start + end) / 2; SumTask leftTask = new SumTask(array, start, mid); SumTask rightTask = new SumTask(array, mid, end); // 并行执行:一个fork,一个当前线程执行 leftTask.fork(); long rightResult = rightTask.compute(); long leftResult = leftTask.join(); return leftResult + rightResult; } } public static void main(String[] args) { long[] array = new long[1000000]; Arrays.fill(array, 1L); // 100万个1 ForkJoinPool pool = new ForkJoinPool(); long result = pool.invoke(new SumTask(array, 0, array.length)); System.out.println("计算结果: " + result); // 输出: 1000000 }}5. 性能对比与选择指南5.1 不同场景的工具选择使用场景推荐工具理由高并发缓存ConcurrentHashMap分段锁,读多写少优化任务队列ConcurrentLinkedQueue无锁,高吞吐量资源池管理LinkedBlockingQueue阻塞操作,流量控制优先级处理PriorityBlockingQueue按优先级排序延时任务DelayQueue支持延时执行直接传递SynchronousQueue零存储,直接传递并行计算Fork/Join框架分治算法,工作窃取5.2 性能优化要点public class PerformanceTips { public void optimizationGuidelines() { // 1. 合理设置容量:避免频繁扩容或内存浪费 // 2. 选择合适的队列:根据业务特性选择 // 3. 避免过度同步:能用无锁就不用有锁 // 4. 注意异常处理:并发环境下的异常传播 // 5. 监控资源使用:避免内存泄漏和资源耗尽 }}6. 最佳实践总结6.1 设计原则解耦生产消费:生产者专注生产,消费者专注消费合理设置边界:防止资源耗尽,保证系统稳定性优雅处理异常:不能让一个线程的异常影响整个系统监控与调优:根据实际负载调整参数6.2 常见陷阱与规避public class CommonPitfalls { public void avoidTheseMistakes() { // ❌ 错误:在并发容器中执行耗时操作 // ✅ 正确:快速完成容器操作,复杂逻辑异步处理 // ❌ 错误:忽略容量边界导致内存溢出 // ✅ 正确:合理设置容量,使用有界队列 // ❌ 错误:依赖size()做业务判断 // ✅ 正确:使用专门的状态变量 // ❌ 错误:在Fork/Join任务中执行IO // ✅ 正确:Fork/Join只用于计算密集型任务 }}结语:掌握并发编程的艺术Java并发容器就像精心设计的交通系统,每种工具都在特定场景下发挥独特价值:ConcurrentHashMap:智能的多车道高速公路ConcurrentLinkedQueue:无锁的立体交通枢纽阻塞队列:有协调员的智能等待区Fork/Join框架:团队协作的分布式工作模式掌握这些工具,你就能构建出既安全又高效的并发程序,真正发挥多核硬件的威力。记住:合适的工具用在合适的场景,这才是并发编程的真谛。现在,拿起这些利器,开始构建你的高性能并发应用吧! 转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19171977
-
Nginx(发音为 "engine-x")已成为高并发、高性能 Web 服务的代名词。作为全球最受欢迎的 Web 服务器之一,Nginx 不仅被 Google、Facebook、Netflix、淘宝、京东等大型互联网公司广泛采用,更是微服务、负载均衡、API 网关等现代架构的核心组件。 一、什么是 Nginx?Nginx 是一个开源的 高性能 HTTP 服务器和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 邮件代理服务器。它由俄罗斯程序员 Igor Sysoev 于 2004 年发布,最初为解决 C10K 问题(单机支持 1 万并发连接)而设计。核心特点:特性说明高性能异步非阻塞事件驱动架构,资源消耗低,支持高并发高可靠性进程模型稳定,即使高负载下也不会崩溃热部署支持不停机更新配置、升级版本模块化设计功能通过模块扩展,灵活可定制反向代理与负载均衡支持多种负载均衡算法静态资源服务高效处理 HTML、CSS、JS、图片等静态文件二、Nginx 架构原理:为什么这么快?1. 事件驱动 + 异步非阻塞与传统 Apache 的 多进程/多线程模型(每个连接占用一个进程/线程)不同,Nginx 采用 事件驱动的异步非阻塞 I/O 模型。Master 进程:管理进程,不处理请求。Worker 进程:每个 Worker 采用单线程 + 事件循环(Event Loop)处理成千上万个并发连接。I/O 多路复用:使用 epoll(Linux)、kqueue(BSD)等机制,一个进程可监听多个 socket。✅ 优势:内存占用少,上下文切换开销小,轻松应对数万并发连接。三、Nginx 的核心应用场景1. 静态 Web 服务器Nginx 是服务静态资源的绝佳选择,性能远超应用服务器(如 Tomcat)。server { listen 80; server_name www.example.com; location / { root /var/www/html; # 静态文件目录 index index.html; } # 缓存静态资源 location ~* \.(jpg|jpeg|png|css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; }}2. 反向代理(Reverse Proxy)Nginx 作为“门面”,接收客户端请求,转发给后端应用服务器(如 Java、Python、Node.js),并返回响应。server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; # 转发到本地 8080 端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}✅ 优势:隐藏后端服务器真实 IP统一入口,便于管理提升安全性3. 负载均衡(Load Balancing)Nginx 可将请求分发到多个后端服务器,实现横向扩展与高可用。配置示例:upstream backend { # 负载均衡算法 least_conn; # 最少连接 # round-robin; # 轮询(默认) # ip_hash; # IP 哈希(会话保持) # hash $request_uri; # 一致性哈希 server 192.168.1.10:8080 weight=3; # 权重 3 server 192.168.1.11:8080; server 192.168.1.12:8080 backup; # 备用服务器}server { listen 80; location / { proxy_pass http://backend; }}4. SSL/TLS 加密(HTTPS)Nginx 可作为 SSL 终端,处理 HTTPS 请求并解密后转发给后端 HTTP 服务。server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/private.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512; location / { proxy_pass http://backend; }}✅ 推荐:使用 Let's Encrypt 免费证书 + Certbot 自动续期。5. 缓存加速Nginx 支持反向代理缓存,减少后端压力,提升响应速度。# 定义缓存区proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;server { location / { proxy_cache my_cache; proxy_pass http://backend; proxy_cache_valid 200 302 10m; # 缓存 10 分钟 add_header X-Cache-Status $upstream_cache_status; }}缓存命中时,X-Cache-Status 返回 HIT,否则为 MISS。6. URL 重写与重定向# 301 永久重定向rewrite ^/old-page$ /new-page permanent;# 条件重写if ($http_user_agent ~* "bot|spider") { rewrite ^/.*$ /robots.txt break;}# 伪静态rewrite ^/article/(\d+)\.html$ /article.php?id=$1 last;四、常用配置指令详解指令作用listen监听端口和 IPserver_name匹配域名location定义 URL 路由规则root / alias文件路径映射proxy_pass反向代理目标upstream定义后端服务器组try_files尝试多个文件路径(常用于 SPA 路由)gzip启用 Gzip 压缩SPA 应用路由支持(如 Vue、React)location / { root /var/www/app; try_files $uri $uri/ /index.html;}确保前端路由刷新不 404。五、性能优化建议1. Worker 进程优化worker_processes auto; # 通常设置为 CPU 核心数worker_connections 1024; # 每个 Worker 最大连接数worker_rlimit_nofile 65535; # 提升文件描述符限制2. 开启 Gzip 压缩gzip on;gzip_types text/plain text/css application/json application/javascript text/xml application/xml;3. 启用 HTTP/2listen 443 ssl http2;减少延迟,提升加载速度。4. 静态资源缓存location ~* \.(css|js|jpg|png|gif)$ { expires 1y; add_header Cache-Control "public, immutable";}六、安全加固1. 隐藏 Nginx 版本号server_tokens off;2. 防止点击劫持add_header X-Frame-Options SAMEORIGIN;3. 防止 XSS 攻击add_header X-Content-Type-Options nosniff;add_header Content-Security-Policy "default-src 'self'";4. 限制请求频率limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;location /api/ { limit_req zone=api burst=20 nodelay;}
-
一篇笔记主要介绍 gin.Engine,设置路由等操作,以下是本篇笔记目录:gin.Default() 和 gin.New()HTTP 方法路由分组与中间件1、gin.Default() 和 gin.New()前面第一篇笔记介绍,创建一个 gin 的路由引擎使用的函数是 gin.Default(),返回的类型是 *gin.Engine,我们可以使用其创建路由和路由组。除了这个函数外,还有一个 gin.New(),其返回的也是 *gin.Engine,但是不一样的是 gin.Default() 会对 gin.Engine 添加默认的 Logger() 和 Recovery() 中间件。这两个函数大致内容如下:func New(opts ...OptionFunc) *Engine { ...}func Default(opts ...OptionFunc) *Engine { ... engine := New() engine.Use(Logger(), Recovery()) ...}我们使用第一篇笔记中使用 debug 模式运行系统后输出的信息可以再看一下:[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] GET /test --> main.main.func1 (3 handlers)[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.[GIN-debug] Listening and serving HTTP on :9898我们使用的是 gin.Default() 运行的系统,所以在第一行告诉我们创建了一个带有 Logger and Recovery 中间件的 Engine。同时第三行输出路由信息的地方,标明了这个路由指向的处理函数,后面的括号里是 3 handlers,这个意思是除了我们处理路由的 handler,还有两个默认的中间件 handler,也就是这里的 Logger() 和 Recovery() 中间件。下面介绍一下 Logger() 和 Recovery() 这两个 handler 的作用。1. Logger()默认的 Logger() 会输出接口调用的信息,比如第一篇中我们定义了一个 /test 接口,当我们调用这个接口的时候,控制台会输出下面这条信息:[GIN] 2025/08/19 - 23:15:26 | 200 | 36.666µs | 127.0.0.1 | GET "/test"可以看到日志中会包含请求时间、返回的 HTTP 状态码、请求耗时、调用方 ip、请求方式和接口名称等。这条日志信息的输出就是 Logger() 这个中间件起的作用。在其内部,会调用一个 LoggerWithConfig() 函数,获取到请求的 ip、记录调用时间、调用方式等信息,然后进行输出,下面是部分源码信息:param.TimeStamp = time.Now()param.Latency = param.TimeStamp.Sub(start)param.ClientIP = c.ClientIP()param.Method = c.Request.Methodparam.StatusCode = c.Writer.Status()param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()2. Recovery()Recovery() 中间件则可以为我们捕获程序中未处理的 panic,记录错误信息并返回 500 状态码信息,比如我们在第一篇笔记中使用的 TestHandler 函数,我们在其中加一个除数为 0 的错误:func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } a := 0 fmt.Println(1 / a) c.JSON(http.StatusOK, response)}在接口调用的时候,如果我们使用的是 gin.Default(),那么客户端不会报错,而是会收到一个 HTTP 状态码为 500 的报错信息,而如果使用的是 gin.New(),客户端则会直接发生错误。总的来说,Logger() 和 Recovery() 这两个的中间件是 gin 框架为我们默认添加的对于开发者来说较为友好的两个操作,在后面介绍中间件的时候,我们也可以手动实现这两个功能。2、HTTP 方法gin.Engine 支持配置 HTTP 多个方法,比如 GET、POST、PUT、DELETE 等。以第一篇笔记中的代码为例,其设置方法如下:r.GET("/test", TestHandler)r.POST("/test", TestHandler)r.PUT("/test", TestHandler)r.DELETE("/test", TestHandler)3、路由分组与中间件除了设置单个路由,我们还可以对路由进行分组设置,比如需要控制版本,或者模块设置需要统一的前缀,又或者是需要统一设置中间件功能的时候。其整体代码示例如下:package mainimport ( "fmt" "net/http" "github.com/gin-gonic/gin")type TestResponse struct { Code int `json:"code"` Message string `json:"message"`}func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } c.JSON(http.StatusOK, response)}func main() { r := gin.Default() v1 := r.Group("/v1") { v1.GET("/test", TestHandler) } err := r.Run(":9898") if err != nil { fmt.Println("gin run in 9898 error:", err) }}这里,我们设置了一个路由名称以 v1 为前缀的路由组,其下每个路由的访问都需要带有 /v1,这样就实现了统一设置路由前缀的功能。而如果我们需要向其中添加中间件的时候,也可以不用挨个路由进行设置,而是在 v1 路由组的设置中就可以实现,比如:v1 := r.Group("/v1", Middleware1, Middleware2)这样,其下每个路由的 handler 函数在调用前就都会先调用 Middleware1 和 Middleware2 这两个中间件。以上就是本篇笔记关于 gin.Engine 的全部内容,其实中间件的相关操作也应该属于 gin.Engine 的内容,但是那部分需要介绍的知识点和想要用于介绍的代码示例略多,所以就单独开一篇笔记在后面再介绍。 转载于:https://www.cnblogs.com/hunterxiong/p/19175625
-
在多线程并发环境下,保证数据操作的原子性是个常见且关键的挑战。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,其中包含13个强大的原子操作类,让我们能够以无锁的方式实现线程安全。本文将带你深入理解这些原子类的原理、API和使用场景。一、为什么需要原子操作类?1.1 问题的由来想象一下这样的场景:多个线程同时操作同一个银行账户进行取款,如果不加控制,可能会出现什么情况?// 不安全的计数器示例class UnsafeCounter { private int count = 0; public void increment() { count++; // 这不是原子操作! }}count++看似简单,实际上包含三个步骤:读取count的当前值将值加1将新值写回count在多线程环境下,这两个步骤可能被其他线程打断,导致数据不一致。1.2 传统的解决方案及其缺点传统做法是使用synchronized关键字:class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; }}synchronized确实能保证线程安全,但存在以下问题:性能开销:锁的获取和释放需要代价可能死锁:不正确的锁顺序可能导致死锁降低并发性:同一时刻只有一个线程能访问1.3 原子操作类的优势原子操作类基于CAS(Compare-And-Swap) 机制,提供了:无锁编程:避免传统锁的开销高性能:在低竞争环境下性能优异无死锁风险:基于硬件指令,不会产生死锁高并发:支持多个线程同时操作二、原子更新基本类型类2.1 AtomicBoolean - 原子更新布尔类型使用场景:状态标志位、开关控制、条件判断核心API详解方法参数返回值说明get()-boolean获取当前值set(boolean newValue)newValue: 新值void设置新值getAndSet(boolean newValue)newValue: 新值boolean原子性地设置为新值并返回旧值compareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean如果当前值等于期望值,则原子性地更新lazySet(boolean newValue)newValue: 新值void最终设置为新值,但不保证立即可见性weakCompareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean可能更弱的CAS操作,在某些平台上性能更好import java.util.concurrent.atomic.AtomicBoolean;/** * AtomicBoolean示例:用于原子性地更新布尔值 * 典型场景:系统开关、状态标志等 */public class AtomicBooleanDemo { public static void main(String[] args) { // 创建AtomicBoolean,初始值为false AtomicBoolean atomicBoolean = new AtomicBoolean(false); // get(): 获取当前值 System.out.println("初始值: " + atomicBoolean.get()); // getAndSet(): 原子性地设置为true,返回旧值 boolean oldValue = atomicBoolean.getAndSet(true); System.out.println("getAndSet旧值: " + oldValue + ", 新值: " + atomicBoolean.get()); // compareAndSet(): 比较并设置 boolean success = atomicBoolean.compareAndSet(true, false); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicBoolean.get()); // lazySet(): 最终会设置,但不保证立即可见性 atomicBoolean.lazySet(true); System.out.println("lazySet后的值: " + atomicBoolean.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicBoolean.weakCompareAndSet(true, false); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前值: " + atomicBoolean.get()); }}原理分析:AtomicBoolean内部实际上使用int类型来存储,0表示false,1表示true。通过compareAndSwapInt来实现原子操作。2.2 AtomicInteger - 原子更新整型使用场景:计数器、序列号生成、资源数量控制核心API详解方法参数返回值说明get()-int获取当前值set(int newValue)newValue: 新值void设置新值getAndSet(int newValue)newValue: 新值int原子性地设置为新值并返回旧值compareAndSet(int expect, int update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-int原子递增,返回旧值getAndDecrement()-int原子递减,返回旧值getAndAdd(int delta)delta: 增量int原子加法,返回旧值incrementAndGet()-int原子递增,返回新值decrementAndGet()-int原子递减,返回新值addAndGet(int delta)delta: 增量int原子加法,返回新值updateAndGet(IntUnaryOperator)operator: 更新函数int函数式更新accumulateAndGet(int x, IntBinaryOperator)x: 参数operator: 操作函数int累积计算import java.util.concurrent.atomic.AtomicInteger;/** * AtomicInteger是最常用的原子类之一 * 适用于计数器、ID生成器等需要原子递增的场景 */public class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(0); // 基础操作 System.out.println("初始值: " + atomicInt.get()); atomicInt.set(5); System.out.println("set(5)后: " + atomicInt.get()); // 原子递增并返回旧值 - 常用于计数 System.out.println("getAndIncrement: " + atomicInt.getAndIncrement()); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 6 // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicInt.getAndDecrement()); // 返回6 System.out.println("当前值: " + atomicInt.get()); // 5 // 原子加法并返回旧值 System.out.println("getAndAdd(10): " + atomicInt.getAndAdd(10)); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 15 // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicInt.incrementAndGet()); // 16 // 原子加法并返回结果 - 适合批量增加 int result = atomicInt.addAndGet(10); System.out.println("addAndGet(10)结果: " + result); // 26 // 比较并设置 - 核心CAS操作 boolean updated = atomicInt.compareAndSet(26, 30); System.out.println("CAS操作结果: " + updated + ", 当前值: " + atomicInt.get()); // 获取并设置新值 - 适合重置操作 int previous = atomicInt.getAndSet(40); System.out.println("getAndSet旧值: " + previous + ", 新值: " + atomicInt.get()); // JDK8新增:函数式更新 - 更灵活的更新方式 atomicInt.updateAndGet(x -> x * 2); System.out.println("updateAndGet(*2)后的值: " + atomicInt.get()); // 80 // 累积计算 atomicInt.accumulateAndGet(10, (x, y) -> x + y * 2); System.out.println("accumulateAndGet后的值: " + atomicInt.get()); // 100 }}源码分析:public final int getAndIncrement() { // 自旋CAS:循环直到成功 for (;;) { int current = get(); // 步骤1:获取当前值 int next = current + 1; // 步骤2:计算新值 if (compareAndSet(current, next)) // 步骤3:CAS更新 return current; // 成功则返回旧值 } // 如果CAS失败,说明有其他线程修改了值,循环重试}2.3 AtomicLong - 原子更新长整型使用场景:大数值计数器、统计信息、唯一ID生成核心API详解方法参数返回值说明get()-long获取当前值set(long newValue)newValue: 新值void设置新值getAndSet(long newValue)newValue: 新值long原子性地设置为新值并返回旧值compareAndSet(long expect, long update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-long原子递增,返回旧值getAndDecrement()-long原子递减,返回旧值getAndAdd(long delta)delta: 增量long原子加法,返回旧值incrementAndGet()-long原子递增,返回新值decrementAndGet()-long原子递减,返回新值addAndGet(long delta)delta: 增量long原子加法,返回新值updateAndGet(LongUnaryOperator)operator: 更新函数long函数式更新accumulateAndGet(long x, LongBinaryOperator)x: 参数operator: 操作函数long累积计算import java.util.concurrent.atomic.AtomicLong;/** * AtomicLong用于长整型的原子操作 * 在64位系统中性能与AtomicInteger相当 */public class AtomicLongDemo { public static void main(String[] args) { AtomicLong atomicLong = new AtomicLong(100L); System.out.println("初始值: " + atomicLong.get()); // 原子递增并返回旧值 - 适合序列号生成 System.out.println("getAndIncrement: " + atomicLong.getAndIncrement()); System.out.println("当前值: " + atomicLong.get()); // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicLong.getAndDecrement()); System.out.println("当前值: " + atomicLong.get()); // 原子加法并返回旧值 System.out.println("getAndAdd(50): " + atomicLong.getAndAdd(50L)); System.out.println("当前值: " + atomicLong.get()); // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicLong.incrementAndGet()); // 原子加法并返回结果 - 适合统计累加 long newValue = atomicLong.addAndGet(50L); System.out.println("addAndGet(50)结果: " + newValue); // 比较并设置 boolean success = atomicLong.compareAndSet(250L, 300L); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicLong.get()); // JDK8新增:函数式更新 atomicLong.updateAndGet(x -> x / 2); System.out.println("updateAndGet(/2)后的值: " + atomicLong.get()); // JDK8新增:累积计算 - 适合复杂的原子计算 atomicLong.accumulateAndGet(100L, (x, y) -> x * y); System.out.println("accumulateAndGet后的值: " + atomicLong.get()); }}性能提示:在32位系统上,AtomicLong的CAS操作可能需要锁住总线,性能相对较差。Java 8提供了LongAdder作为高性能替代方案。三、原子更新数组类3.1 AtomicIntegerArray - 原子更新整型数组使用场景:并发计数器数组、桶统计、并行计算核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引int获取指定索引的值set(int i, int newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, int newValue)i: 索引newValue: 新值int原子设置并返回旧值compareAndSet(int i, int expect, int update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndIncrement(int i)i: 索引int原子递增指定索引,返回旧值getAndDecrement(int i)i: 索引int原子递减指定索引,返回旧值getAndAdd(int i, int delta)i: 索引delta: 增量int原子加法,返回旧值incrementAndGet(int i)i: 索引int原子递增指定索引,返回新值addAndGet(int i, int delta)i: 索引delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerArray;/** * AtomicIntegerArray允许原子地更新数组中的单个元素 * 注意:构造函数会复制传入的数组,不影响原数组 */public class AtomicIntegerArrayDemo { public static void main(String[] args) { int[] initialArray = {1, 2, 3, 4, 5}; // 创建原子整型数组,会复制传入的数组 AtomicIntegerArray atomicArray = new AtomicIntegerArray(initialArray); System.out.println("数组长度: " + atomicArray.length()); System.out.println("原始数组: " + atomicArray.toString()); // get(): 获取指定索引的值 System.out.println("索引0的值: " + atomicArray.get(0)); // set(): 设置指定索引的值 atomicArray.set(0, 10); System.out.println("set(0, 10)后的数组: " + atomicArray.toString()); // getAndSet(): 原子更新指定索引的元素并返回旧值 int oldValue = atomicArray.getAndSet(1, 20); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicArray.toString()); // getAndIncrement(): 原子递增指定索引的元素 - 适合分桶计数 oldValue = atomicArray.getAndIncrement(2); System.out.println("索引2递增前值: " + oldValue + ", 数组: " + atomicArray.toString()); // compareAndSet(): 比较并设置特定位置的元素 boolean updated = atomicArray.compareAndSet(3, 4, 40); System.out.println("索引3 CAS结果: " + updated + ", 数组: " + atomicArray.toString()); // addAndGet(): 原子加法 - 适合累加统计 int newValue = atomicArray.addAndGet(4, 5); System.out.println("索引4加5后的值: " + newValue + ", 数组: " + atomicArray.toString()); // incrementAndGet(): 原子递增并返回新值 newValue = atomicArray.incrementAndGet(0); System.out.println("索引0递增后的值: " + newValue); // 重要:原始数组不会被修改 System.out.println("原始数组值未被修改: " + initialArray[0]); // 仍然是1 }}设计思想:AtomicIntegerArray通过复制数组来避免外部修改,每个数组元素的更新都是独立的原子操作。3.2 AtomicLongArray - 原子更新长整型数组使用场景:大数据统计、时间戳数组、大数值桶统计核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引long获取指定索引的值set(int i, long newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, long newValue)i: 索引newValue: 新值long原子设置并返回旧值compareAndSet(int i, long expect, long update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndAdd(int i, long delta)i: 索引delta: 增量long原子加法,返回旧值addAndGet(int i, long delta)i: 索引delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongArray;/** * AtomicLongArray提供长整型数组的原子操作 * 适用于需要大数值范围的并发统计 */public class AtomicLongArrayDemo { public static void main(String[] args) { long[] initialArray = {100L, 200L, 300L, 400L, 500L}; AtomicLongArray atomicLongArray = new AtomicLongArray(initialArray); System.out.println("数组长度: " + atomicLongArray.length()); System.out.println("初始数组: " + atomicLongArray.toString()); // 基础操作 System.out.println("索引0的值: " + atomicLongArray.get(0)); atomicLongArray.set(0, 150L); System.out.println("set(0, 150)后的数组: " + atomicLongArray.toString()); // 原子更新操作 long oldValue = atomicLongArray.getAndSet(1, 250L); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicLongArray.toString()); // 原子加法操作 atomicLongArray.getAndAdd(2, 100L); System.out.println("索引2加100后的数组: " + atomicLongArray.toString()); // 比较并设置 atomicLongArray.compareAndSet(3, 400L, 450L); System.out.println("索引3 CAS后的数组: " + atomicLongArray.toString()); // 加法并获取新值 long newValue = atomicLongArray.addAndGet(4, 200L); System.out.println("索引4加200后的值: " + newValue); }}3.3 AtomicReferenceArray - 原子更新引用类型数组使用场景:对象池、缓存数组、并发数据结构核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引E获取指定索引的引用set(int i, E newValue)i: 索引newValue: 新引用void设置指定索引的引用getAndSet(int i, E newValue)i: 索引newValue: 新引用E原子设置并返回旧引用compareAndSet(int i, E expect, E update)i: 索引expect: 期望引用update: 更新引用boolean对指定索引进行CAS操作lazySet(int i, E newValue)i: 索引newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceArray;/** * AtomicReferenceArray用于原子更新引用类型数组 * 适用于对象引用需要原子更新的场景 */public class AtomicReferenceArrayDemo { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } } public static void main(String[] args) { Person[] persons = { new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 35), new Person("David", 40) }; AtomicReferenceArray<Person> atomicArray = new AtomicReferenceArray<>(persons); System.out.println("数组长度: " + atomicArray.length()); System.out.println("初始数组: "); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } // 原子更新引用 - 适合对象替换 Person newPerson = new Person("Eve", 28); Person oldPerson = atomicArray.getAndSet(1, newPerson); System.out.println("索引1替换: " + oldPerson + " -> " + atomicArray.get(1)); // 比较并设置引用 boolean success = atomicArray.compareAndSet(2, persons[2], new Person("Frank", 45)); System.out.println("索引2 CAS结果: " + success + ", 新值: " + atomicArray.get(2)); // 延迟设置 atomicArray.lazySet(3, new Person("Grace", 50)); System.out.println("索引3延迟设置后的值: " + atomicArray.get(3)); // 遍历数组 System.out.println("最终数组状态:"); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } }}四、原子更新引用类型4.1 AtomicReference - 原子更新引用类型使用场景:单例模式、缓存更新、状态对象替换核心API详解方法参数返回值说明get()-V获取当前引用set(V newValue)newValue: 新引用void设置新引用getAndSet(V newValue)newValue: 新引用V原子设置并返回旧引用compareAndSet(V expect, V update)expect: 期望引用update: 更新引用booleanCAS操作weakCompareAndSet(V expect, V update)expect: 期望引用update: 更新引用boolean弱版本CASlazySet(V newValue)newValue: 新引用void延迟设置引用updateAndGet(UnaryOperator<V>)operator: 更新函数V函数式更新getAndUpdate(UnaryOperator<V>)operator: 更新函数V函数式更新并返回旧值accumulateAndGet(V x, BinaryOperator<V>)x: 参数operator: 操作函数V累积计算import java.util.concurrent.atomic.AtomicReference;/** * AtomicReference用于原子更新对象引用 * 解决"先检查后执行"的竞态条件 */public class AtomicReferenceDemo { static class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } } public static void main(String[] args) { AtomicReference<User> atomicUser = new AtomicReference<>(); User initialUser = new User("张三", 25); atomicUser.set(initialUser); System.out.println("初始用户: " + atomicUser.get()); // getAndSet(): 原子更新引用 - 适合缓存更新 User newUser = new User("李四", 30); User oldUser = atomicUser.getAndSet(newUser); System.out.println("替换前的用户: " + oldUser); System.out.println("当前用户: " + atomicUser.get()); // compareAndSet(): 比较并设置 - 核心操作 boolean success = atomicUser.compareAndSet(newUser, new User("王五", 35)); System.out.println("CAS操作结果: " + success + ", 当前用户: " + atomicUser.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicUser.weakCompareAndSet( atomicUser.get(), new User("赵六", 40)); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前用户: " + atomicUser.get()); // lazySet(): 延迟设置 atomicUser.lazySet(new User("孙七", 45)); System.out.println("延迟设置后的用户: " + atomicUser.get()); // JDK8新增:函数式更新 atomicUser.updateAndGet(user -> new User(user.getName() + "_updated", user.getAge() + 1)); System.out.println("函数式更新后的用户: " + atomicUser.get()); // getAndUpdate(): 函数式更新并返回旧值 User previous = atomicUser.getAndUpdate(user -> new User("周八", 50)); System.out.println("更新前的用户: " + previous + ", 当前用户: " + atomicUser.get()); // accumulateAndGet(): 累积计算 atomicUser.accumulateAndGet(new User("吴九", 55), (old, param) -> new User(old.getName() + "&" + param.getName(), old.getAge() + param.getAge())); System.out.println("累积计算后的用户: " + atomicUser.get()); }}典型应用:单例模式的双重检查锁定class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>(); public static Singleton getInstance() { for (;;) { Singleton current = INSTANCE.get(); if (current != null) return current; current = new Singleton(); if (INSTANCE.compareAndSet(null, current)) { return current; } } }}4.2 AtomicMarkableReference - 带标记位的原子引用使用场景:带状态的缓存、ABA问题简单解决方案核心API详解方法参数返回值说明getReference()-V获取当前引用isMarked()-boolean获取当前标记位get(boolean[] markHolder)markHolder: 标记位容器V获取引用和标记位set(V newReference, boolean newMark)newReference: 新引用newMark: 新标记void设置引用和标记位compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)expectedReference: 期望引用newReference: 新引用expectedMark: 期望标记newMark: 新标记boolean同时比较引用和标记位attemptMark(V expectedReference, boolean newMark)expectedReference: 期望引用newMark: 新标记boolean尝试只更新标记位import java.util.concurrent.atomic.AtomicMarkableReference;/** * AtomicMarkableReference将引用与一个布尔标记位绑定 * 适用于需要同时更新引用和状态的场景 */public class AtomicMarkableReferenceDemo { public static void main(String[] args) { String initialRef = "初始数据"; boolean initialMark = false; // 创建带标记位的原子引用 AtomicMarkableReference<String> atomicMarkableRef = new AtomicMarkableReference<>(initialRef, initialMark); System.out.println("初始引用: " + atomicMarkableRef.getReference()); System.out.println("初始标记: " + atomicMarkableRef.isMarked()); // get(boolean[]): 同时获取引用和标记位 boolean[] markHolder = new boolean[1]; String currentRef = atomicMarkableRef.get(markHolder); System.out.println("当前引用: " + currentRef + ", 当前标记: " + markHolder[0]); // compareAndSet(): 尝试同时更新引用和标记位 String newRef = "新数据"; boolean newMark = true; boolean success = atomicMarkableRef.compareAndSet( initialRef, newRef, initialMark, newMark); System.out.println("CAS操作结果: " + success); System.out.println("新引用: " + atomicMarkableRef.getReference()); System.out.println("新标记: " + atomicMarkableRef.isMarked()); // attemptMark(): 只尝试更新标记位 boolean markUpdated = atomicMarkableRef.attemptMark(newRef, false); System.out.println("标记更新结果: " + markUpdated); System.out.println("最终标记: " + atomicMarkableRef.isMarked()); // set(): 直接设置引用和标记位 atomicMarkableRef.set("最终数据", true); System.out.println("直接设置后的引用: " + atomicMarkableRef.getReference()); System.out.println("直接设置后的标记: " + atomicMarkableRef.isMarked()); }}4.3 AtomicStampedReference - 带版本号的原子引用使用场景:解决ABA问题、乐观锁实现核心API详解方法参数返回值说明getReference()-V获取当前引用getStamp()-int获取当前版本号get(int[] stampHolder)stampHolder: 版本号容器V获取引用和版本号set(V newReference, int newStamp)newReference: 新引用newStamp: 新版本号void设置引用和版本号compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)expectedReference: 期望引用newReference: 新引用expectedStamp: 期望版本号newStamp: 新版本号boolean同时比较引用和版本号attemptStamp(V expectedReference, int newStamp)expectedReference: 期望引用newStamp: 新版本号boolean尝试只更新版本号import java.util.concurrent.atomic.AtomicStampedReference;/** * AtomicStampedReference通过版本号解决ABA问题 * 每次修改都会增加版本号,确保不会误判 */public class AtomicStampedReferenceDemo { public static void main(String[] args) { String initialRef = "数据A"; int initialStamp = 0; // 创建带版本号的原子引用 AtomicStampedReference<String> atomicStampedRef = new AtomicStampedReference<>(initialRef, initialStamp); System.out.println("初始引用: " + atomicStampedRef.getReference()); System.out.println("初始版本号: " + atomicStampedRef.getStamp()); // get(int[]): 同时获取引用和版本号 int[] stampHolder = new int[1]; String currentRef = atomicStampedRef.get(stampHolder); System.out.println("当前引用: " + currentRef + ", 当前版本号: " + stampHolder[0]); // 模拟ABA问题场景 String newRefB = "数据B"; String newRefA = "数据A"; // 又改回A,但版本号不同 // 第一次更新:A -> B,版本号 0 -> 1 boolean firstUpdate = atomicStampedRef.compareAndSet( initialRef, newRefB, initialStamp, initialStamp + 1); System.out.println("第一次更新(A->B)结果: " + firstUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 第二次更新:B -> A,版本号 1 -> 2 boolean secondUpdate = atomicStampedRef.compareAndSet( newRefB, newRefA, 1, 2); System.out.println("第二次更新(B->A)结果: " + secondUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 尝试用旧版本号更新(会失败)- 这就是解决ABA问题的关键! boolean failedUpdate = atomicStampedRef.compareAndSet( newRefA, "新数据", 0, 1); // 使用旧的版本号0 System.out.println("使用旧版本号更新结果: " + failedUpdate); System.out.println("引用未被修改: " + atomicStampedRef.getReference()); // attemptStamp(): 只更新版本号 boolean stampUpdated = atomicStampedRef.attemptStamp(newRefA, 3); System.out.println("版本号更新结果: " + stampUpdated); System.out.println("新版本号: " + atomicStampedRef.getStamp()); // 正确的方式:使用当前版本号 stampHolder = new int[1]; currentRef = atomicStampedRef.get(stampHolder); boolean correctUpdate = atomicStampedRef.compareAndSet( currentRef, "最终数据", stampHolder[0], stampHolder[0] + 1); System.out.println("使用正确版本号更新结果: " + correctUpdate); System.out.println("最终引用: " + atomicStampedRef.getReference()); System.out.println("最终版本号: " + atomicStampedRef.getStamp()); }}ABA问题详解:ABA问题是指:线程1读取值A线程2将值改为B,然后又改回A线程1进行CAS操作,发现当前值仍是A,于是操作成功虽然值看起来没变,但中间状态的变化可能对业务逻辑产生影响。AtomicStampedReference通过版本号完美解决了这个问题。五、原子更新字段类5.1 AtomicIntegerFieldUpdater - 原子更新整型字段使用场景:优化内存使用、大量对象需要原子字段更新核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicIntegerFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象int获取字段值set(U obj, int newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, int newValue)obj: 目标对象newValue: 新值int原子设置并返回旧值compareAndSet(U obj, int expect, int update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndIncrement(U obj)obj: 目标对象int原子递增,返回旧值getAndDecrement(U obj)obj: 目标对象int原子递减,返回旧值getAndAdd(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回旧值incrementAndGet(U obj)obj: 目标对象int原子递增,返回新值addAndGet(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;/** * AtomicIntegerFieldUpdater以原子方式更新对象的volatile int字段 * 相比为每个对象创建AtomicInteger,可以节省大量内存 */public class AtomicIntegerFieldUpdaterDemo { static class Counter { // 必须用volatile修饰,保证可见性 public volatile int count; private String name; public Counter(String name, int initialCount) { this.name = name; this.count = initialCount; } public String getName() { return name; } public int getCount() { return count; } } public static void main(String[] args) { // 创建字段更新器,指定要更新的类和字段名 AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count"); Counter counter1 = new Counter("计数器1", 0); Counter counter2 = new Counter("计数器2", 10); System.out.println("计数器1初始计数: " + counter1.getCount()); System.out.println("计数器2初始计数: " + counter2.getCount()); // get(): 获取字段值 System.out.println("通过updater获取计数器1的值: " + updater.get(counter1)); // set(): 设置字段值 updater.set(counter1, 5); System.out.println("设置计数器1为5后的值: " + counter1.getCount()); // getAndIncrement(): 原子递增 - 相比synchronized性能更好 int oldCount = updater.getAndIncrement(counter1); System.out.println("计数器1递增前值: " + oldCount + ", 当前值: " + counter1.getCount()); // getAndAdd(): 原子加法 oldCount = updater.getAndAdd(counter1, 10); System.out.println("计数器1加10前值: " + oldCount + ", 当前值: " + counter1.getCount()); // incrementAndGet(): 原子递增并返回新值 int newCount = updater.incrementAndGet(counter1); System.out.println("计数器1递增后的值: " + newCount); // addAndGet(): 原子加法并返回新值 newCount = updater.addAndGet(counter1, 20); System.out.println("计数器1加20后的值: " + newCount); // compareAndSet(): 比较并设置 boolean updated = updater.compareAndSet(counter1, 36, 50); System.out.println("计数器1 CAS操作结果: " + updated + ", 当前值: " + counter1.getCount()); // 可以同时更新多个对象的相同字段 updater.incrementAndGet(counter2); System.out.println("计数器2递增后的值: " + counter2.getCount()); }}内存优化效果:AtomicInteger对象:16-24字节 overheadvolatile int + AtomicIntegerFieldUpdater:4字节 + 静态updater当有大量对象时,内存节省效果显著5.2 AtomicLongFieldUpdater - 原子更新长整型字段使用场景:大数值字段的原子更新、内存敏感场景核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicLongFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象long获取字段值set(U obj, long newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, long newValue)obj: 目标对象newValue: 新值long原子设置并返回旧值compareAndSet(U obj, long expect, long update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndAdd(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回旧值addAndGet(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongFieldUpdater;/** * AtomicLongFieldUpdater用于原子更新long字段 * 适用于需要大数值范围且内存敏感的场景 */public class AtomicLongFieldUpdaterDemo { static class Account { // 必须用volatile修饰 public volatile long balance; private final String owner; public Account(String owner, long initialBalance) { this.owner = owner; this.balance = initialBalance; } public String getOwner() { return owner; } public long getBalance() { return balance; } } public static void main(String[] args) { AtomicLongFieldUpdater<Account> balanceUpdater = AtomicLongFieldUpdater.newUpdater(Account.class, "balance"); Account account1 = new Account("张三", 1000L); Account account2 = new Account("李四", 2000L); System.out.println("张三账户初始余额: " + account1.getBalance()); System.out.println("李四账户初始余额: " + account2.getBalance()); // 基础操作 System.out.println("通过updater获取张三余额: " + balanceUpdater.get(account1)); balanceUpdater.set(account1, 1500L); System.out.println("设置张三余额为1500后的值: " + account1.getBalance()); // 原子存款 - 无锁线程安全 balanceUpdater.addAndGet(account1, 500L); System.out.println("张三存款500后余额: " + account1.getBalance()); // 原子取款 long oldBalance = balanceUpdater.getAndAdd(account1, -200L); System.out.println("张三取款200前余额: " + oldBalance + ", 取款后余额: " + account1.getBalance()); // 比较并设置 - 实现转账等业务 boolean transferSuccess = balanceUpdater.compareAndSet(account1, 1800L, 2000L); System.out.println("张三转账操作结果: " + transferSuccess + ", 当前余额: " + account1.getBalance()); // 同时操作多个账户 balanceUpdater.getAndAdd(account2, 1000L); System.out.println("李四存款1000后余额: " + account2.getBalance()); }}5.3 AtomicReferenceFieldUpdater - 原子更新引用字段使用场景:链表节点更新、树结构调整、对象关系维护核心API详解方法参数返回值说明newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)tclass: 目标类vclass: 字段类型fieldName: 字段名AtomicReferenceFieldUpdater<U,W>静态方法创建更新器get(U obj)obj: 目标对象V获取字段引用set(U obj, V newValue)obj: 目标对象newValue: 新引用void设置字段引用getAndSet(U obj, V newValue)obj: 目标对象newValue: 新引用V原子设置并返回旧引用compareAndSet(U obj, V expect, V update)obj: 目标对象expect: 期望引用update: 更新引用booleanCAS操作lazySet(U obj, V newValue)obj: 目标对象newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * AtomicReferenceFieldUpdater用于原子更新引用字段 * 常用于实现无锁数据结构 */public class AtomicReferenceFieldUpdaterDemo { static class Node<T> { // 必须用volatile修饰 public volatile Node<T> next; private final T value; public Node(T value) { this.value = value; } public T getValue() { return value; } public Node<T> getNext() { return next; } @Override public String toString() { return "Node{value=" + value + ", next=" + (next != null ? next.value : "null") + "}"; } } public static void main(String[] args) { // 创建引用字段更新器 AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); Node<String> first = new Node<>("第一个节点"); Node<String> second = new Node<>("第二个节点"); Node<String> third = new Node<>("第三个节点"); System.out.println("初始第一个节点的next: " + first.getNext()); // get(): 获取字段引用 System.out.println("通过updater获取第一个节点的next: " + nextUpdater.get(first)); // set(): 设置字段引用 nextUpdater.set(first, second); System.out.println("设置第一个节点的next为第二个节点: " + first); // compareAndSet(): 原子设置next字段 - 实现无锁链表 boolean setSuccess = nextUpdater.compareAndSet(first, second, third); System.out.println("CAS操作结果: " + setSuccess); System.out.println("第一个节点: " + first); // getAndSet(): 获取并设置引用 Node<String> oldNext = nextUpdater.getAndSet(second, third); System.out.println("第二个节点原来的next: " + oldNext); System.out.println("第二个节点: " + second); // lazySet(): 延迟设置 nextUpdater.lazySet(third, first); // 形成环状,仅作演示 System.out.println("第三个节点延迟设置后的next: " + third.getNext()); // 构建链表并展示 System.out.println("最终链表结构:"); Node<String> current = first; int count = 0; while (current != null && count < 5) { // 防止无限循环 System.out.println(current); current = current.getNext(); count++; } }}六、综合实战:构建线程安全计数器下面我们通过一个综合示例展示如何在实际项目中使用原子操作类:import java.util.concurrent.atomic.*;import java.util.concurrent.*;/** * 线程安全计数器综合示例 * 展示了多种原子类的实际应用 */public class ThreadSafeCounter { // 基本计数器 - 使用AtomicInteger private final AtomicInteger count = new AtomicInteger(0); // 大数值统计 - 使用AtomicLong private final AtomicLong total = new AtomicLong(0L); // 状态控制 - 使用AtomicReference private final AtomicReference<String> status = new AtomicReference<>("RUNNING"); // 统计数组 - 使用AtomicIntegerArray进行分桶统计 private final AtomicIntegerArray bucketStats = new AtomicIntegerArray(10); // 配置信息 - 使用AtomicReference支持动态更新 private final AtomicReference<Config> config = new AtomicReference<>(new Config(100, 60)); // 标记位控制 - 使用AtomicBoolean private final AtomicBoolean enabled = new AtomicBoolean(true); static class Config { final int maxConnections; final int timeoutSeconds; public Config(int maxConnections, int timeoutSeconds) { this.maxConnections = maxConnections; this.timeoutSeconds = timeoutSeconds; } @Override public String toString() { return "Config{maxConnections=" + maxConnections + ", timeoutSeconds=" + timeoutSeconds + "}"; } } // 核心API方法 public void increment() { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.incrementAndGet(); total.addAndGet(1L); // 分桶统计:根据count值决定放入哪个桶 int bucket = count.get() % 10; bucketStats.getAndIncrement(bucket); } public void add(int value) { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.addAndGet(value); total.addAndGet(value); } public boolean setStatus(String expected, String newStatus) { return status.compareAndSet(expected, newStatus); } public void updateConfig(Config newConfig) { Config oldConfig; do { oldConfig = config.get(); System.out.println("尝试更新配置: " + oldConfig + " -> " + newConfig); } while (!config.compareAndSet(oldConfig, newConfig)); System.out.println("配置更新成功"); } public boolean enable() { return enabled.compareAndSet(false, true); } public boolean disable() { return enabled.compareAndSet(true, false); } // 获取统计信息 public void printStats() { System.out.println("\n=== 统计信息 ==="); System.out.println("当前计数: " + count.get()); System.out.println("总数: " + total.get()); System.out.println("状态: " + status.get()); System.out.println("启用状态: " + enabled.get()); System.out.println("桶统计: " + bucketStats.toString()); Config currentConfig = config.get(); System.out.println("配置: " + currentConfig); // 验证数据一致性 long sum = 0; for (int i = 0; i < bucketStats.length(); i++) { sum += bucketStats.get(i); } System.out.println("桶统计总和: " + sum + ", 计数: " + count.get() + ", 一致性: " + (sum == count.get())); } public static void main(String[] args) throws InterruptedException { ThreadSafeCounter counter = new ThreadSafeCounter(); // 创建多个线程同时操作计数器 int threadCount = 10; int operationsPerThread = 1000; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); System.out.println("开始并发测试..."); for (int i = 0; i < threadCount; i++) { final int threadId = i; executor.execute(() -> { try { for (int j = 0; j < operationsPerThread; j++) { counter.increment(); // 每隔一定操作数更新配置 if (j % 200 == 0) { counter.updateConfig(new Config(100 + j, 60)); } // 模拟随机禁用/启用 if (j == 500 && threadId == 0) { System.out.println("线程" + threadId + "尝试禁用计数器"); counter.disable(); Thread.sleep(10); // 短暂休眠 System.out.println("线程" + threadId + "尝试启用计数器"); counter.enable(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } // 等待所有线程完成 latch.await(); executor.shutdown(); // 打印最终统计 counter.printStats(); int expectedCount = threadCount * operationsPerThread; System.out.println("\n=== 测试结果 ==="); System.out.println("期望计数: " + expectedCount); System.out.println("实际计数: " + counter.count.get()); System.out.println("计数正确: " + (counter.count.get() == expectedCount)); System.out.println("测试" + (counter.count.get() == expectedCount ? "通过" : "失败")); }}七、原子操作类的工作原理7.1 CAS机制详解CAS(Compare-And-Swap)是原子操作类的核心,包含三个操作数:内存位置(V)期望原值(A)新值(B)CAS的语义是:"我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉我现在的值是多少"CAS操作是硬件级别的原子操作,在现代CPU中通常通过以下方式实现:x86架构:CMPXCHG指令ARM架构:LDREX/STREX指令对7.2 Unsafe类的作用所有原子操作类底层都依赖sun.misc.Unsafe类,它提供了硬件级别的原子操作:public final class Unsafe { // 对象字段操作 public native long objectFieldOffset(Field f); // 数组基础偏移 public native int arrayBaseOffset(Class arrayClass); // 数组索引缩放 public native int arrayIndexScale(Class arrayClass); // CAS操作 public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); // 获取和设置值 public native int getIntVolatile(Object o, long offset); public native void putIntVolatile(Object o, long offset, int x); // 延迟设置(有更弱的可见性保证) public native void putOrderedInt(Object o, long offset, int x);}7.3 内存屏障与可见性原子操作类通过内存屏障保证可见性:写操作:在写入后插入写屏障,保证写入对其他线程可见读操作:在读取前插入读屏障,保证读取到最新值Java内存模型中的屏障类型:LoadLoad屏障:保证该屏障前的读操作先于屏障后的读操作完成StoreStore屏障:保证该屏障前的写操作先于屏障后的写操作完成LoadStore屏障:保证该屏障前的读操作先于屏障后的写操作完成StoreLoad屏障:保证该屏障前的所有写操作对其他处理器可见八、性能对比与选型建议8.1 性能对比场景synchronized原子操作类性能提升低竞争慢快2-10倍中等竞争中等中等相当高竞争快慢(自旋)可能更差8.2 不同原子类的性能特点原子类适用场景性能特点AtomicInteger普通计数器性能优秀,适用大部分场景AtomicLong大数值计数在32位系统上性能较差LongAdder高并发统计高竞争环境下性能最优AtomicReference对象引用更新性能与对象大小相关字段更新器内存敏感场景节省内存,性能稍差8.3 选型指南计数器场景简单计数:AtomicInteger大数值计数:AtomicLong 或 LongAdder分桶统计:AtomicIntegerArray状态控制布尔标志:AtomicBoolean对象状态:AtomicReference带版本状态:AtomicStampedReference内存敏感场景大量对象:字段更新器(AtomicXXXFieldUpdater)缓存系统:AtomicReference数据结构无锁队列:AtomicReference无锁栈:AtomicReference无锁链表:AtomicReferenceFieldUpdater8.4 最佳实践避免过度使用:不是所有场景都需要原子类注意ABA问题:必要时使用带版本号的原子类考虑高竞争:高竞争环境下考虑LongAdder等替代方案内存布局:字段更新器可以优化内存使用JDK8+特性:利用新的函数式更新方法性能测试:在实际环境中进行性能测试九、总结Java原子操作类为我们提供了强大的无锁并发编程工具:9.1 核心价值13个原子类覆盖了基本类型、数组、引用和字段更新CAS机制基于硬件指令,性能优异无锁设计避免了死锁和锁开销丰富的API支持各种并发场景9.2 使用场景总结类别主要类核心用途基本类型AtomicInteger, AtomicLong, AtomicBoolean计数器、状态标志数组AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray并发数组、分桶统计引用AtomicReference, AtomicStampedReference, AtomicMarkableReference对象缓存、状态管理字段更新AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater内存优化、大量对象9.3 学习建议从简单开始:先掌握AtomicInteger和AtomicReference理解原理:深入理解CAS机制和内存模型实践应用:在真实项目中尝试使用原子类性能调优:根据实际场景选择合适的原子类持续学习:关注JDK新版本中的并发工具改进掌握这些原子操作类,能够让我们在适当的场景下写出更高效、更安全的并发代码。记住,工具虽好,但要因地制宜,根据具体场景选择最合适的并发控制方案。希望本文能帮助你深入理解Java原子操作类,在实际项目中游刃有余地处理并发问题! 转载于:https://www.cnblogs.com/sun-10387834/p/19172186
-
Spring Boot 目前已成为构建企业级应用的事实标准。它以“约定优于配置”(Convention over Configuration)为核心理念,极大地简化了 Spring 应用的初始搭建和开发过程,让开发者能够专注于业务逻辑,而非繁琐的配置。 一、什么是 Spring Boot?Spring Boot 是由 Pivotal 团队提供的一个开源框架,它基于 Spring 框架 构建,旨在:简化 Spring 应用的创建和部署。提供开箱即用的默认配置,减少样板代码。内嵌服务器(如 Tomcat、Jetty),无需打包成 WAR 部署到外部容器。提供生产级特性,如健康检查、指标监控、外部化配置等。一句话总结:Spring Boot 让 Spring 应用的开发像“搭积木”一样简单,开箱即用,快速启动。二、为什么选择 Spring Boot?1. 自动配置(Auto-configuration)Spring Boot 能根据你添加的依赖(如 spring-boot-starter-web)自动配置 Spring 应用。例如:添加了 Web 依赖,它会自动配置:内嵌 Tomcat 服务器Spring MVC默认的视图解析器错误页面处理你无需手动编写大量 XML 或 Java 配置。2. 起步依赖(er Dependencies)Spring Boot 提供了一系列“starter”依赖,将常用的依赖组合在一起,避免版本冲突。Starter功能spring-boot-starter-webWeb + REST + Tomcatspring-boot-starter-data-jpaJPA + Hibernatespring-boot-starter-data-redisRedis 客户端spring-boot-starter-securitySpring Securityspring-boot-starter-test测试支持只需引入一个依赖,即可获得一整套功能。3. 内嵌服务器无需将应用打包成 WAR 文件部署到 Tomcat、Jetty 等外部服务器。Spring Boot 应用自带服务器,打包成 JAR 即可运行:java -jar myapp.jar4. 生产就绪(Production Ready)Spring Boot 提供了 Spring Boot Actuator 模块,开箱即用的监控和管理功能:/actuator/health:应用健康状态/actuator/metrics:性能指标/actuator/env:当前环境变量/actuator/info:自定义应用信息5. 外部化配置支持多种方式配置应用参数:application.propertiesapplication.yml环境变量命令行参数并支持多环境配置(如 application-dev.yml, application-prod.yml)。6. 强大的 CLI 和代码生成工具Spring Initializr:在线生成项目骨架。Spring Boot CLI:命令行工具,快速运行 Groovy 脚本。三、快速创建一个 Spring Boot 应用方法1:使用 Spring Initializr访问 官方网站选择:Project: Maven / GradleLanguage: JavaSpring Boot Version: 最新稳定版Group: com.exampleArtifact: demo添加依赖:Spring Web, Spring Boot DevTools, Lombok点击 “Generate” 下载项目压缩包。方法2:使用 IDE(如 IntelliJ IDEA)File → New → Project选择 “Spring Initializr”填写项目信息并选择依赖完成创建四、项目结构解析src/├── main/│ ├── java/│ │ └── com/example/demo/│ │ ├── DemoApplication.java # 主启动类│ │ └── controller/│ │ └── HelloController.java # 控制器示例│ └── resources/│ ├── application.yml # 配置文件│ ├── static/ # 静态资源│ └── templates/ # 模板文件(如 Thymeleaf)└── test/ # 测试代码五、编写第一个 REST API// HelloController.java@RestController // = @Controller + @ResponseBody@RequestMapping("/api")public class HelloController { @GetMapping("/hello") public String sayHello() { return "Hello, Spring Boot!"; } @GetMapping("/user") public Map<String, Object> getUser() { Map<String, Object> user = new HashMap<>(); user.put("id", 1); user.put("name", "Alice"); user.put("email", "alice@example.com"); return user; }}启动应用后访问:http://localhost:8080/api/hello → 输出 Hello, Spring Boot!http://localhost:8080/api/user → 返回 JSON 数据六、核心配置文件(application.yml)server: port: 8080 servlet: context-path: /appspring: datasource: url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true# 自定义属性app: name: My Awesome App version: 1.0.0可通过 @Value("${app.name}") 或 @ConfigurationProperties 注入。七、Spring Boot 的核心注解注解说明@SpringBootApplication主类注解,包含 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan@RestController创建 RESTful 控制器@RequestMapping映射 HTTP 请求@Autowired自动注入 Bean@Component, @Service, @Repository组件注册@ConfigurationProperties绑定配置文件属性八、开发与部署开发阶段使用 spring-boot-devtools 实现热部署(代码修改自动重启)。使用 @Profile 切换开发/测试/生产环境。打包与运行# Maven 打包mvn clean package# 运行 JARjava -jar target/demo-0.0.1-SNAPSHOT.jar# 指定配置文件java -jar app.jar --spring.profiles.active=prod Spring Boot 已成为 Java 开发的标准工具链,无论是构建简单的 Web 应用,还是复杂的微服务系统,它都能提供强大的支持。其“约定优于配置”的理念,让开发者从繁琐的配置中解放出来,真正实现“快速开发、快速交付”。
-
Apache POI 是用Java编写的免费开源的跨平台的Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能,其中使用最多的就是使用POI操作Excel文件。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。一、运营数据统计1.1 需求分析通过运营数据统计可以展示出体检机构的运营情况,包括会员数据、预约到诊数据、热门套餐等信息。本章节就是要通过一个表格的形式来展示这些运营数据。效果如下图: 1.2 完善页面运营数据统计对应的页面为/pages/report_business.html。1.2.1 定义模型数据定义数据模型,通过VUE的数据绑定展示数据<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } } }) </script><div class="box" style="height: 900px"> <div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div> <div class="excelTime">日期:{{reportData.reportDate}}</div> <table class="exceTable" cellspacing="0" cellpadding="0"> <tr> <td colspan="4" class="headBody">会员数据统计</td> </tr> <tr> <td width='20%' class="tabletrBg">新增会员数</td> <td width='30%'>{{reportData.todayNewMember}}</td> <td width='20%' class="tabletrBg">总会员数</td> <td width='30%'>{{reportData.totalMember}}</td> </tr> <tr> <td class="tabletrBg">本周新增会员数</td> <td>{{reportData.thisWeekNewMember}}</td> <td class="tabletrBg">本月新增会员数</td> <td>{{reportData.thisMonthNewMember}}</td> </tr> <tr> <td colspan="4" class="headBody">预约到诊数据统计</td> </tr> <tr> <td class="tabletrBg">今日预约数</td> <td>{{reportData.todayOrderNumber}}</td> <td class="tabletrBg">今日到诊数</td> <td>{{reportData.todayVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本周预约数</td> <td>{{reportData.thisWeekOrderNumber}}</td> <td class="tabletrBg">本周到诊数</td> <td>{{reportData.thisWeekVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本月预约数</td> <td>{{reportData.thisMonthOrderNumber}}</td> <td class="tabletrBg">本月到诊数</td> <td>{{reportData.thisMonthVisitsNumber}}</td> </tr> <tr> <td colspan="4" class="headBody">热门套餐</td> </tr> <tr class="tabletrBg textCenter"> <td>套餐名称</td> <td>预约数量</td> <td>占比</td> <td>备注</td> </tr> <tr v-for="s in reportData.hotSetmeal"> <td>{{s.name}}</td> <td>{{s.setmeal_count}}</td> <td>{{s.proportion}}</td> <td></td> </tr> </table> </div>1.2.2 发送请求获取动态数据在VUE的钩子函数中发送ajax请求获取动态数据,通过VUE的数据绑定将数据展示到页面<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } }, created() { //发送ajax请求获取动态数据 axios.get("/report/getBusinessReportData.do").then((res)=>{ this.reportData = res.data.data; }); } }) </script>根据页面对数据格式的要求,我们发送ajax请求,服务端需要返回如下格式的数据:{ "data":{ "todayVisitsNumber":0, "reportDate":"2019-04-25", "todayNewMember":0, "thisWeekVisitsNumber":0, "thisMonthNewMember":2, "thisWeekNewMember":0, "totalMember":10, "thisMonthOrderNumber":2, "thisMonthVisitsNumber":0, "todayOrderNumber":0, "thisWeekOrderNumber":0, "hotSetmeal":[ {"proportion":0.4545,"name":"粉红珍爱(女)升级TM12项筛查体检套餐","setmeal_count":5}, {"proportion":0.1818,"name":"阳光爸妈升级肿瘤12项筛查体检套餐","setmeal_count":2}, {"proportion":0.1818,"name":"珍爱高端升级肿瘤12项筛查","setmeal_count":2}, {"proportion":0.0909,"name":"孕前检查套餐","setmeal_count":1} ], }, "flag":true, "message":"获取运营统计数据成功" }1.3 后台代码1.3.1 Controller在ReportController中提供getBusinessReportData方法@Reference private ReportService reportService; /** * 获取运营统计数据 * @return */ @RequestMapping("/getBusinessReportData") public Result getBusinessReportData(){ try { Map<String, Object> result = reportService.getBusinessReport(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_SUCCESS,result); } catch (Exception e) { e.printStackTrace(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_FAIL); } }1.3.2 服务接口在health_interface工程中创建ReportService服务接口并声明getBusinessReport方法package com.yunhe.service; import java.util.Map; public interface ReportService { /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeals -> List<Setmeal> */ public Map<String,Object> getBusinessReport() throws Exception; }1.3.3 服务实现类在health_service_provider工程中创建服务实现类ReportServiceImpl并实现ReportService接口package com.yunhe.service; import com.alibaba.dubbo.config.annotation.Service; import com.yunhe.dao.MemberDao; import com.yunhe.dao.OrderDao; import com.yunhe.utils.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 统计报表服务 */ @Service(interfaceClass = ReportService.class) @Transactional public class ReportServiceImpl implements ReportService { @Autowired private MemberDao memberDao; @Autowired private OrderDao orderDao; /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeal -> List<Setmeal> */ public Map<String, Object> getBusinessReport() throws Exception{ //获得当前日期 String today = DateUtils.parseDate2String(DateUtils.getToday()); //获得本周一的日期 String thisWeekMonday = DateUtils.parseDate2String(DateUtils.getThisWeekMonday()); //获得本月第一天的日期 String firstDay4ThisMonth = DateUtils.parseDate2String(DateUtils.getFirstDay4ThisMonth()); //今日新增会员数 Integer todayNewMember = memberDao.findMemberCountByDate(today); //总会员数 Integer totalMember = memberDao.findMemberTotalCount(); //本周新增会员数 Integer thisWeekNewMember = memberDao.findMemberCountAfterDate(thisWeekMonday); //本月新增会员数 Integer thisMonthNewMember = memberDao.findMemberCountAfterDate(firstDay4ThisMonth); //今日预约数 Integer todayOrderNumber = orderDao.findOrderCountByDate(today); //本周预约数 Integer thisWeekOrderNumber = orderDao.findOrderCountAfterDate(thisWeekMonday); //本月预约数 Integer thisMonthOrderNumber = orderDao.findOrderCountAfterDate(firstDay4ThisMonth); //今日到诊数 Integer todayVisitsNumber = orderDao.findVisitsCountByDate(today); //本周到诊数 Integer thisWeekVisitsNumber = orderDao.findVisitsCountAfterDate(thisWeekMonday); //本月到诊数 Integer thisMonthVisitsNumber = orderDao.findVisitsCountAfterDate(firstDay4ThisMonth); //热门套餐(取前4) List<Map> hotSetmeal = orderDao.findHotSetmeal(); Map<String,Object> result = new HashMap<>(); result.put("reportDate",today); result.put("todayNewMember",todayNewMember); result.put("totalMember",totalMember); result.put("thisWeekNewMember",thisWeekNewMember); result.put("thisMonthNewMember",thisMonthNewMember); result.put("todayOrderNumber",todayOrderNumber); result.put("thisWeekOrderNumber",thisWeekOrderNumber); result.put("thisMonthOrderNumber",thisMonthOrderNumber); result.put("todayVisitsNumber",todayVisitsNumber); result.put("thisWeekVisitsNumber",thisWeekVisitsNumber); result.put("thisMonthVisitsNumber",thisMonthVisitsNumber); result.put("hotSetmeal",hotSetmeal); return result; } }1.3.4 Dao接口在OrderDao和MemberDao中声明相关统计查询方法package com.yunhe.dao; import com.yunhe.pojo.Order; import java.util.List; import java.util.Map; public interface OrderDao { public void add(Order order); public List<Order> findByCondition(Order order); public Map findById4Detail(Integer id); public Integer findOrderCountByDate(String date); public Integer findOrderCountAfterDate(String date); public Integer findVisitsCountByDate(String date); public Integer findVisitsCountAfterDate(String date); public List<Map> findHotSetmeal(); }package com.yunhe.dao; import com.github.pagehelper.Page; import com.yunhe.pojo.Member; import java.util.List; public interface MemberDao { public List<Member> findAll(); public Page<Member> selectByCondition(String queryString); public void add(Member member); public void deleteById(Integer id); public Member findById(Integer id); public Member findByTelephone(String telephone); public void edit(Member member); public Integer findMemberCountBeforeDate(String date); public Integer findMemberCountByDate(String date); public Integer findMemberCountAfterDate(String date); public Integer findMemberTotalCount(); }1.3.5 Mapper映射文件在OrderDao.xml和MemberDao.xml中定义SQL语句OrderDao.xml:<!--根据日期统计预约数--> <select id="findOrderCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} </select> <!--根据日期统计预约数,统计指定日期之后的预约数--> <select id="findOrderCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} </select> <!--根据日期统计到诊数--> <select id="findVisitsCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} and orderStatus = '已到诊' </select> <!--根据日期统计到诊数,统计指定日期之后的到诊数--> <select id="findVisitsCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} and orderStatus = '已到诊' </select> <!--热门套餐,查询前4条--> <select id="findHotSetmeal" resultType="map"> select s.name, count(o.id) setmeal_count , count(o.id)/(select count(id) from t_order) proportion from t_order o inner join t_setmeal s on s.id = o.setmeal_id group by o.setmeal_id order by setmeal_count desc limit 0,4 </select>MemberDao.xml:<!--根据日期统计会员数,统计指定日期之前的会员数--> <select id="findMemberCountBeforeDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime <= #{value} </select> <!--根据日期统计会员数--> <select id="findMemberCountByDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime = #{value} </select> <!--根据日期统计会员数,统计指定日期之后的会员数--> <select id="findMemberCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime >= #{value} </select> <!--总会员数--> <select id="findMemberTotalCount" resultType="int"> select count(id) from t_member </select>二、. 运营数据统计报表导出2.1 需求分析运营数据统计报表导出就是将统计数据写入到Excel并提供给客户端浏览器进行下载,以便体检机构管理人员对运营数据的查看和存档。2.2 提供模板文件本节我们需要将运营统计数据通过POI写入到Excel文件,对应的Excel效果如下: 通过上面的Excel效果可以看到,表格比较复杂,涉及到合并单元格、字体、字号、字体加粗、对齐方式等的设置。如果我们通过POI编程的方式来设置这些效果代码会非常繁琐。在企业实际开发中,对于这种比较复杂的表格导出一般我们会提前设计一个Excel模板文件,在这个模板文件中提前将表格的结构和样式设置好,我们的程序只需要读取这个文件并在文件中的相应位置写入具体的值就可以了。在本章节资料中已经提供了一个名为report_template.xlsx的模板文件,需要将这个文件复制到health_backend工程的template目录中2.3 完善页面在report_business.html页面提供导出按钮并绑定事件<div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div>methods:{ //导出Excel报表 exportExcel(){ window.location.href = '/report/exportBusinessReport.do'; } }2.4 后台代码在ReportController中提供exportBusinessReport方法,基于POI将数据写入到Excel中并通过输出流下载到客户端。/** * 导出Excel报表 * @return */ @RequestMapping("/exportBusinessReport") public Result exportBusinessReport(HttpServletRequest request, HttpServletResponse response){ try{ //远程调用报表服务获取报表数据 Map<String, Object> result = reportService.getBusinessReport(); //取出返回结果数据,准备将报表数据写入到Excel文件中 String reportDate = (String) result.get("reportDate"); Integer todayNewMember = (Integer) result.get("todayNewMember"); Integer totalMember = (Integer) result.get("totalMember"); Integer thisWeekNewMember = (Integer) result.get("thisWeekNewMember"); Integer thisMonthNewMember = (Integer) result.get("thisMonthNewMember"); Integer todayOrderNumber = (Integer) result.get("todayOrderNumber"); Integer thisWeekOrderNumber = (Integer) result.get("thisWeekOrderNumber"); Integer thisMonthOrderNumber = (Integer) result.get("thisMonthOrderNumber"); Integer todayVisitsNumber = (Integer) result.get("todayVisitsNumber"); Integer thisWeekVisitsNumber = (Integer) result.get("thisWeekVisitsNumber"); Integer thisMonthVisitsNumber = (Integer) result.get("thisMonthVisitsNumber"); List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //获得Excel模板文件绝对路径 String temlateRealPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "report_template.xlsx"; //读取模板文件创建Excel表格对象 XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File(temlateRealPath))); XSSFSheet sheet = workbook.getSheetAt(0); XSSFRow row = sheet.getRow(2); row.getCell(5).setCellValue(reportDate);//日期 row = sheet.getRow(4); row.getCell(5).setCellValue(todayNewMember);//新增会员数(本日) row.getCell(7).setCellValue(totalMember);//总会员数 row = sheet.getRow(5); row.getCell(5).setCellValue(thisWeekNewMember);//本周新增会员数 row.getCell(7).setCellValue(thisMonthNewMember);//本月新增会员数 row = sheet.getRow(7); row.getCell(5).setCellValue(todayOrderNumber);//今日预约数 row.getCell(7).setCellValue(todayVisitsNumber);//今日到诊数 row = sheet.getRow(8); row.getCell(5).setCellValue(thisWeekOrderNumber);//本周预约数 row.getCell(7).setCellValue(thisWeekVisitsNumber);//本周到诊数 row = sheet.getRow(9); row.getCell(5).setCellValue(thisMonthOrderNumber);//本月预约数 row.getCell(7).setCellValue(thisMonthVisitsNumber);//本月到诊数 int rowNum = 12; for(Map map : hotSetmeal){//热门套餐 String name = (String) map.get("name"); Long setmeal_count = (Long) map.get("setmeal_count"); BigDecimal proportion = (BigDecimal) map.get("proportion"); row = sheet.getRow(rowNum ++); row.getCell(4).setCellValue(name);//套餐名称 row.getCell(5).setCellValue(setmeal_count);//预约数量 row.getCell(6).setCellValue(proportion.doubleValue());//占比 } //通过输出流进行文件下载 ServletOutputStream out = response.getOutputStream(); response.setContentType("application/vnd.ms-excel"); response.setHeader("content-Disposition", "attachment;filename=report.xlsx"); workbook.write(out); out.flush(); out.close(); workbook.close(); return null; }catch (Exception e){ return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL,null); } }
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签