news 2026/6/7 15:55:37

rpc节点: synchronized (this) + 双检锁,在 race condition 的情况下分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
rpc节点: synchronized (this) + 双检锁,在 race condition 的情况下分析

结合rpc节点刷新业务,讲解 Java 中synchronized (this)的作用、原理和在代码里的具体意义。

这段代码的核心逻辑回顾

publicSolanaRpcClientclient(){SolanaRpcClientclient=this.healthyClient;// 定期检查健康状态if(client==null||System.currentTimeMillis()-lastHealthCheck.get()>HEALTH_CHECK_INTERVAL_MS){synchronized(this){// ← 这里client=this.healthyClient;if(client==null||System.currentTimeMillis()-lastHealthCheck.get()>HEALTH_CHECK_INTERVAL_MS){refreshHealthyClient();client=this.healthyClient;}}}returnclient;}

这段代码的目的是:在多线程环境下安全地获取一个健康的 SolanaRpcClient,并且每隔 30 秒(HEALTH_CHECK_INTERVAL_MS)检查一次健康状态,如果过期或为空,就刷新一次。

synchronized (this) 到底在干什么?

synchronized (this)是一种对象监视器锁(monitor lock),它的作用是:

  • 保证同一时刻只有一个线程能进入这个 synchronized 块
  • 其他线程如果也想进入同一个 synchronized (this) 块,就必须等待锁被释放。

在这里,this指的是当前对象实例(即 SolanaRpcClientService 这个 Spring @Service bean 的实例)。

所以这段代码的锁范围是:

  • 所有调用client()方法的线程,如果同时判断需要刷新,就会竞争同一个对象(this)的锁。
  • 只有一个线程能成功进入 synchronized 块,去执行refreshHealthyClient()

为什么需要加 synchronized (this)?

因为这段代码运行在多线程环境(Spring Boot 应用默认是多线程的,Web 请求、定时任务、消息队列消费者等都可能并发调用client())。

如果不加锁,会出现以下经典的竞态条件(race condition):

场景模拟(不加锁的情况)

假设 HEALTH_CHECK_INTERVAL_MS 刚好过期,healthyClient 还是旧的(或 null)。

线程 A 和线程 B 几乎同时调用client()

  1. 线程 A:看到 client == null 或过期 → 进入 if
  2. 线程 B:也看到 client == null 或过期 → 进入 if
  3. 线程 A:调用 refreshHealthyClient() → 刷新成功,healthyClient 被赋值为新客户端
  4. 线程 B:也调用 refreshHealthyClient() → 又刷新一次(重复工作,浪费资源)
  5. 更糟的情况:如果 refreshHealthyClient() 内部有非线程安全的操作(比如修改共享状态),可能导致数据不一致或异常。

即使 refreshHealthyClient() 本身是线程安全的,重复调用也会浪费网络请求(多次 getSlot 检查),增加延迟。

加了synchronized (this)后:

  • 线程 A 先抢到锁 → 进入 synchronized 块 → 刷新 healthyClient
  • 线程 B 在 synchronized 块门口等待
  • 线程 A 刷新完退出锁 → 线程 B 进入 → 发现 healthyClient 已经是最新的(或刚刷新过),时间戳也更新了 → 直接跳过 if,不再重复刷新

synchronized (this) 的具体保护范围

synchronized(this){client=this.healthyClient;// 重新读取(防止指令重排序)if(client==null||过期){// 双重检查(Double-Checked Locking 模式)refreshHealthyClient();// 只有第一个线程会真正刷新client=this.healthyClient;// 刷新后重新读取}}
  • 双重检查锁定(Double-Checked Locking):这是经典的懒加载/缓存刷新模式。
    • 第一次检查(if)在锁外 → 减少不必要的加锁
    • 第二次检查(if)在锁内 → 保证线程安全
  • volatile修饰 healthyClient 确保可见性(其他线程能看到最新的值)
  • AtomicLong的 lastHealthCheck 也保证原子更新时间戳

为什么用 synchronized (this) 而不是其他锁?

方式优点缺点是否适合这里
synchronized (this)简单、直观、代码量少锁粒度是整个对象(如果类有其他 synchronized 方法,会竞争)适合(当前场景简单)
private final Object lock = new Object(); synchronized (lock)锁粒度更细,不会影响其他方法多写一行代码更好(推荐)
ReentrantLock可中断、可公平、可条件等待代码复杂没必要
synchronized 方法更简洁锁粒度更大(整个方法)不如块好

推荐改进:为了降低锁竞争风险,建议改成专用锁对象:

privatefinalObjecthealthLock=newObject();publicSolanaRpcClientclient(){SolanaRpcClientclient=this.healthyClient;if(client==null||System.currentTimeMillis()-lastHealthCheck.get()>HEALTH_CHECK_INTERVAL_MS){synchronized(healthLock){// ← 改成专用锁client=this.healthyClient;if(client==null||System.currentTimeMillis()-lastHealthCheck.get()>HEALTH_CHECK_INTERVAL_MS){refreshHealthyClient();client=this.healthyClient;}}}returnclient;}

这样即使类里以后加了其他 synchronized 方法,也不会和健康检查锁冲突。

总结一句话

synchronized (this)在这里的作用是:

  • 防止多个线程同时重复调用 refreshHealthyClient(),避免浪费资源和潜在的并发问题。
  • 配合双重检查锁定,确保高并发下健康客户端的刷新是线程安全的,而且只刷新一次。

这是 Java 多线程编程中非常经典的“缓存刷新 + 懒加载”模式,在 RPC 客户端、配置中心、连接池等高可用组件中几乎都会用到。

下边进行定时刷新的逻辑

好的,使用ScheduledExecutorService定时刷新健康 RPC 节点,而不是原来的“按需刷新”(即调用 client() 时才检查),是一个更推荐的生产级方案。

为什么定时刷新更好?

  • 按需刷新的缺点:

    • 第一次调用或过期时,所有并发请求都会竞争锁 → 可能导致“惊群效应”(thundering herd)
    • 高并发场景下,refresh 操作会被频繁触发,浪费资源
    • 延迟敏感:如果正好在过期瞬间调用,可能会短暂阻塞
  • 定时刷新的优点:

    • 刷新是后台异步的,不阻塞业务线程
    • 业务调用 client() 时几乎总是拿到最新的健康节点(无锁、无等待)
    • 资源消耗可控(固定周期,比如每 30 秒刷新一次)
    • 更容易监控和添加告警(刷新失败可以发 metrics 或日志)

下面是基于原有代码的完整改造版本,使用ScheduledExecutorService定时刷新。

packagecom.forms.sdk.client.rpc;importcom.forms.sdk.config.SolanaRpcConfig;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.DisposableBean;importorg.springframework.stereotype.Service;importsoftware.sava.rpc.json.http.client.SolanaRpcClient;importjavax.annotation.PostConstruct;importjava.net.URI;importjava.net.http.HttpClient;importjava.time.Duration;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.concurrent.Executors;importjava.util.concurrent.ScheduledExecutorService;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.atomic.AtomicReference;@ServicepublicclassSolanaRpcClientServiceimplementsDisposableBean{privatestaticfinalLoggerlog=LoggerFactory.getLogger(SolanaRpcClientService.class);privatefinalList<String>rpcUrls;privatefinalDurationconnectTimeout;privatefinalDurationrequestTimeout;// 当前健康的客户端列表(AtomicReference 保证可见性)privatefinalAtomicReference<List<SolanaRpcClient>>healthyClientsRef=newAtomicReference<>(Collections.emptyList());// 当前首选客户端(轮询或随机用)privatefinalAtomicReference<SolanaRpcClient>primaryClientRef=newAtomicReference<>();// 定时刷新器privatefinalScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor(r->{Threadt=newThread(r,"Solana-RPC-HealthChecker");t.setDaemon(true);returnt;});publicSolanaRpcClientService(SolanaRpcConfigconfig){this.rpcUrls=config.getRpcEndpoints();if(rpcUrls==null||rpcUrls.isEmpty()){thrownewIllegalStateException("No Solana RPC URLs configured!");}this.connectTimeout=config.getRpcConnectTimeout()!=null?config.getRpcConnectTimeout():Duration.ofSeconds(30);this.requestTimeout=config.getRpcRequestTimeout()!=null?config.getRpcRequestTimeout():Duration.ofSeconds(60);log.info("SolanaRpcClientService initialized with {} endpoints: {}",rpcUrls.size(),rpcUrls);}@PostConstructpublicvoidinit(){// 启动时立即刷新一次refreshHealthyClients();// 每 30 秒定时刷新(可配置)scheduler.scheduleAtFixedRate(this::refreshHealthyClients,0,30,TimeUnit.SECONDS);log.info("Started periodic RPC health check every 30 seconds");}@Overridepublicvoiddestroy(){// Spring 容器关闭时优雅停止定时器scheduler.shutdown();try{if(!scheduler.awaitTermination(5,TimeUnit.SECONDS)){scheduler.shutdownNow();}}catch(InterruptedExceptione){scheduler.shutdownNow();Thread.currentThread().interrupt();}log.info("SolanaRpcClientService shutdown completed");}/** * 业务代码调用此方法获取客户端(几乎无锁、高性能) */publicSolanaRpcClientclient(){SolanaRpcClientclient=primaryClientRef.get();if(client!=null){returnclient;}// 极端情况:启动初期或全部节点宕机,降级到任意一个List<SolanaRpcClient>clients=healthyClientsRef.get();if(!clients.isEmpty()){returnclients.get(0);// 或 random}// 如果真的没有可用节点,抛异常或返回 null(业务自行处理)log.error("No available Solana RPC client at the moment");thrownewIllegalStateException("No healthy Solana RPC endpoint available");}/** * 获取所有健康客户端(用于监控或手动选择) */publicList<SolanaRpcClient>getHealthyClients(){returnCollections.unmodifiableList(healthyClientsRef.get());}/** * 后台定时刷新健康节点列表 */privatevoidrefreshHealthyClients(){List<SolanaRpcClient>newHealthy=newArrayList<>();ExceptionlastError=null;for(Stringurl:rpcUrls){try{SolanaRpcClientclient=buildClient(url);// 轻量健康检查client.getSlot().join();newHealthy.add(client);log.debug("Healthy RPC endpoint: {}",url);}catch(Exceptione){lastError=e;log.warn("RPC endpoint unhealthy: {} → {}",url,e.toString());}}if(!newHealthy.isEmpty()){// 更新原子引用healthyClientsRef.set(Collections.unmodifiableList(newHealthy));// 随机或轮询选一个作为 primary(这里用第一个,也可 random)SolanaRpcClientnewPrimary=newHealthy.get(0);primaryClientRef.set(newPrimary);log.info("Refreshed {} healthy RPC endpoints, primary: {}",newHealthy.size(),newHealthy.get(0));}else{log.error("All {} RPC endpoints are down! Last error: {}",rpcUrls.size(),lastError!=null?lastError:"unknown");// 可选:这里可以发告警(Prometheus、邮件等)}}privateSolanaRpcClientbuildClient(Stringurl){HttpClienthttpClient=HttpClient.newBuilder().connectTimeout(connectTimeout).build();returnSolanaRpcClient.createClient(URI.create(url),httpClient);}}

关键变化说明

  1. 使用@PostConstruct启动定时刷新

    • Spring Bean 初始化完成后立即执行一次refreshHealthyClients()
    • 然后每 30 秒定时执行(可配置成 15s、60s 等)
  2. 使用AtomicReference存储健康列表和 primary 客户端

    • 保证多线程可见性,无需 synchronized
    • client()方法几乎无锁,直接读取引用(极高性能)
  3. 实现了DisposableBean接口

    • 容器关闭时优雅停止 scheduler,避免线程泄漏
  4. 去掉了按需检查的 synchronized 块

    • 业务线程不再阻塞在刷新上
    • 刷新失败时业务仍可继续使用旧的 healthyClients(直到下次刷新成功)
  5. 降级策略

    • 如果健康列表为空,client()会抛异常(可改成返回 null 或 fallback 到默认 RPC)

进一步优化建议

  • 可配置刷新间隔:把 30 秒写到配置中config.getHealthCheckInterval()
  • 轮询 primary:每次刷新后 random 或 round-robin 选 primaryClient
  • 监控:集成 Micrometer/Prometheus,暴露 healthyClients.size() 作为 gauge
  • 失败重试:在 refresh 时,对单个节点失败可以加重试(retry 2 次)

这样改造后,RPC 客户端变得更稳定、更高效,也更适合生产环境。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 15:39:28

Vue3登录注册验证码实战

以下是使用 Vue.js 实现登录、注册和验证码功能的完整代码示例。我将逐步解释关键部分&#xff0c;并提供可运行的 HTML 文件代码。代码使用了 Vue 3 的 Composition API&#xff0c;并模拟了验证码生成&#xff08;真实应用中应通过后端 API 获取验证码&#xff09;。 思路说…

作者头像 李华
网站建设 2026/6/5 7:07:41

无线通信:介质访问控制(MAC)技术

第一章&#xff1a;无形架构——通信协议栈中的秩序基石 1.1 OSI参考模型与数据链路层的定位 在现代数字社会的宏大叙事中&#xff0c;信息的流动如同城市的血脉&#xff0c;而介质访问控制&#xff08;Medium Access Control, MAC&#xff09;则是维持这条血脉畅通的智能交通…

作者头像 李华
网站建设 2026/5/28 21:37:46

乱中有序:详解 ALOHA 协议的两种形态

在无线通信的历史长河中&#xff0c;ALOHA 协议有着特殊的地位。它诞生于 1970 年代的夏威夷大学&#xff0c;初衷是为了解决群岛之间分散的计算机如何通过无线电连接到中心主机的问题。 想象一下&#xff0c;夏威夷的各个岛屿之间隔着大海&#xff0c;拉网线是不可能的。最简…

作者头像 李华
网站建设 2026/5/30 19:24:44

当“省钱”逻辑闯入即时战场:平价即时零售将如何重塑行业法则?

出品 | 何玺排版 | 叶媛多个迹象表明&#xff0c;拼多多正加速布局即时零售。近期&#xff0c;拼多多被曝正在内测一项名为“百亿超市”的新业务。据报道&#xff0c;该业务依托百亿补贴体系&#xff0c;以限时限量低价券的形式切入商超零售&#xff0c;目前仅对部分随机用户开…

作者头像 李华
网站建设 2026/5/30 22:50:37

MySQL InnoDB 索引深度解析:从底层原理到性能实战

第一章&#xff1a;引言与 InnoDB 架构概览 在现代后端开发面试和高并发系统设计中&#xff0c;MySQL 索引几乎是必问的“八股文”之首。然而&#xff0c;很多人对索引的理解仅停留在“加个索引能变快”的层面&#xff0c;或者机械地背诵“B树”这个名词。 为什么是 B 树&…

作者头像 李华
网站建设 2026/5/29 2:16:44

2026战略导航:深耕B2B领域的顶级战略咨询机构实战力排行榜

在2026年的战略导航中&#xff0c;深耕B2B领域的顶级战略咨询机构通过综合评估多个维度&#xff0c;展现出其独特的市场竞争力与发展趋势。每家机构在方法论创新上均有所侧重&#xff0c;采用独特的分析模型&#xff0c;帮助客户更好地把握市场机会。同时&#xff0c;行业深度作…

作者头像 李华