结合rpc节点刷新业务,讲解 Java 中 synchronized (this) 的作用、原理和在代码里的具体意义。
这段代码的核心逻辑回顾
java
public SolanaRpcClient client() {
SolanaRpcClient client = 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;
}
}
}
return client;
}
这段代码的目的是:在多线程环境下安全地获取一个健康的 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():
- 线程 A:看到 client == null 或过期 → 进入 if
- 线程 B:也看到 client == null 或过期 → 进入 if
- 线程 A:调用 refreshHealthyClient() → 刷新成功,healthyClient 被赋值为新客户端
- 线程 B:也调用 refreshHealthyClient() → 又刷新一次(重复工作,浪费资源)
- 更糟的情况:如果 refreshHealthyClient() 内部有非线程安全的操作(比如修改共享状态),可能导致数据不一致或异常。
即使 refreshHealthyClient() 本身是线程安全的,重复调用也会浪费网络请求(多次 getSlot 检查),增加延迟。
加了 synchronized (this) 后:
- 线程 A 先抢到锁 → 进入 synchronized 块 → 刷新 healthyClient
- 线程 B 在 synchronized 块门口等待
- 线程 A 刷新完退出锁 → 线程 B 进入 → 发现 healthyClient 已经是最新的(或刚刷新过),时间戳也更新了 → 直接跳过 if,不再重复刷新
synchronized (this) 的具体保护范围
java
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 方法 | 更简洁 | 锁粒度更大(整个方法) | 不如块好 |
推荐改进:为了降低锁竞争风险,建议改成专用锁对象:
java
private final Object healthLock = new Object();
public SolanaRpcClient client() {
SolanaRpcClient client = 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;
}
}
}
return client;
}
这样即使类里以后加了其他 synchronized 方法,也不会和健康检查锁冲突。
总结一句话
synchronized (this) 在这里的作用是:
- 防止多个线程同时重复调用 refreshHealthyClient(),避免浪费资源和潜在的并发问题。
- 配合双重检查锁定,确保高并发下健康客户端的刷新是线程安全的,而且只刷新一次。
这是 Java 多线程编程中非常经典的"缓存刷新 + 懒加载"模式,在 RPC 客户端、配置中心、连接池等高可用组件中几乎都会用到。
下边进行定时刷新的逻辑
好的,使用 ScheduledExecutorService 来定时刷新健康 RPC 节点,而不是原来的"按需刷新"(即调用 client() 时才检查),是一个更推荐的生产级方案。
为什么定时刷新更好?
-
按需刷新的缺点:
- 第一次调用或过期时,所有并发请求都会竞争锁 → 可能导致"惊群效应"(thundering herd)
- 高并发场景下,refresh 操作会被频繁触发,浪费资源
- 延迟敏感:如果正好在过期瞬间调用,可能会短暂阻塞
-
定时刷新的优点:
- 刷新是后台异步的,不阻塞业务线程
- 业务调用 client() 时几乎总是拿到最新的健康节点(无锁、无等待)
- 资源消耗可控(固定周期,比如每 30 秒刷新一次)
- 更容易监控和添加告警(刷新失败可以发 metrics 或日志)
下面是基于原有代码的完整改造版本,使用 ScheduledExecutorService 定时刷新。
java
package com.forms.sdk.client.rpc;
import com.forms.sdk.config.SolanaRpcConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Service;
import software.sava.rpc.json.http.client.SolanaRpcClient;
import javax.annotation.PostConstruct;
import java.net.URI;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class SolanaRpcClientService implements DisposableBean {
private static final Logger log = LoggerFactory.getLogger(SolanaRpcClientService.class);
private final List<String> rpcUrls;
private final Duration connectTimeout;
private final Duration requestTimeout;
// 当前健康的客户端列表(AtomicReference 保证可见性)
private final AtomicReference<List<SolanaRpcClient>> healthyClientsRef =
new AtomicReference<>(Collections.emptyList());
// 当前首选客户端(轮询或随机用)
private final AtomicReference<SolanaRpcClient> primaryClientRef = new AtomicReference<>();
// 定时刷新器
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Solana-RPC-HealthChecker");
t.setDaemon(true);
return t;
});
public SolanaRpcClientService(SolanaRpcConfig config) {
this.rpcUrls = config.getRpcEndpoints();
if (rpcUrls == null || rpcUrls.isEmpty()) {
throw new IllegalStateException("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);
}
@PostConstruct
public void init() {
// 启动时立即刷新一次
refreshHealthyClients();
// 每 30 秒定时刷新(可配置)
scheduler.scheduleAtFixedRate(this::refreshHealthyClients, 0, 30, TimeUnit.SECONDS);
log.info("Started periodic RPC health check every 30 seconds");
}
@Override
public void destroy() {
// Spring 容器关闭时优雅停止定时器
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
log.info("SolanaRpcClientService shutdown completed");
}
/**
* 业务代码调用此方法获取客户端(几乎无锁、高性能)
*/
public SolanaRpcClient client() {
SolanaRpcClient client = primaryClientRef.get();
if (client != null) {
return client;
}
// 极端情况:启动初期或全部节点宕机,降级到任意一个
List<SolanaRpcClient> clients = healthyClientsRef.get();
if (!clients.isEmpty()) {
return clients.get(0); // 或 random
}
// 如果真的没有可用节点,抛异常或返回 null(业务自行处理)
log.error("No available Solana RPC client at the moment");
throw new IllegalStateException("No healthy Solana RPC endpoint available");
}
/**
* 获取所有健康客户端(用于监控或手动选择)
*/
public List<SolanaRpcClient> getHealthyClients() {
return Collections.unmodifiableList(healthyClientsRef.get());
}
/**
* 后台定时刷新健康节点列表
*/
private void refreshHealthyClients() {
List<SolanaRpcClient> newHealthy = new ArrayList<>();
Exception lastError = null;
for (String url : rpcUrls) {
try {
SolanaRpcClient client = buildClient(url);
// 轻量健康检查
client.getSlot().join();
newHealthy.add(client);
log.debug("Healthy RPC endpoint: {}", url);
} catch (Exception e) {
lastError = e;
log.warn("RPC endpoint unhealthy: {} → {}", url, e.toString());
}
}
if (!newHealthy.isEmpty()) {
// 更新原子引用
healthyClientsRef.set(Collections.unmodifiableList(newHealthy));
// 随机或轮询选一个作为 primary(这里用第一个,也可 random)
SolanaRpcClient newPrimary = 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、邮件等)
}
}
private SolanaRpcClient buildClient(String url) {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(connectTimeout)
.build();
return SolanaRpcClient.createClient(URI.create(url), httpClient);
}
}
关键变化说明
-
使用
@PostConstruct启动定时刷新- Spring Bean 初始化完成后立即执行一次
refreshHealthyClients() - 然后每 30 秒定时执行(可配置成 15s、60s 等)
- Spring Bean 初始化完成后立即执行一次
-
使用
AtomicReference存储健康列表和 primary 客户端- 保证多线程可见性,无需 synchronized
client()方法几乎无锁,直接读取引用(极高性能)
-
实现了
DisposableBean接口- 容器关闭时优雅停止 scheduler,避免线程泄漏
-
去掉了按需检查的 synchronized 块
- 业务线程不再阻塞在刷新上
- 刷新失败时业务仍可继续使用旧的 healthyClients(直到下次刷新成功)
-
降级策略
- 如果健康列表为空,
client()会抛异常(可改成返回 null 或 fallback 到默认 RPC)
- 如果健康列表为空,
进一步优化建议
- 可配置刷新间隔 :把 30 秒写到配置中
config.getHealthCheckInterval() - 轮询 primary:每次刷新后 random 或 round-robin 选 primaryClient
- 监控:集成 Micrometer/Prometheus,暴露 healthyClients.size() 作为 gauge
- 失败重试:在 refresh 时,对单个节点失败可以加重试(retry 2 次)
这样改造后,RPC 客户端变得更稳定、更高效,也更适合生产环境。