首先说结论,在分布式系统中,
单纯使用 Java 中的
synchronized
关键字是无法满足需求的
,下面从
synchronized
的作用原理、在分布式场景下的局限性以及替代方案等方面详细分析。
在 Java 中,
synchronized
关键字用于实现线程同步,它可以保证在同一时刻,只有一个线程能够访问被
synchronized
修饰的代码块或方法。其本质是通过获取对象的监视器(
monitor
)来实现互斥访问,这是基于 JVM 层面的同步机制,作用范围仅限于单个 JVM 进程内。
以下是一个简单的
synchronized
使用示例:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + example.count);
}
}
在这个示例中,
increment
方法被
synchronized
修饰,确保了在同一时刻只有一个线程能够执行该方法,从而避免了多线程环境下的竞态条件。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
分布式系统由多个独立的 JVM 进程组成,不同进程之间无法直接共享对象的监视器。
synchronized
只能保证单个 JVM 内的线程同步,无法实现跨 JVM 进程的同步。
因此,在分布式系统中,如果多个进程同时访问共享资源,使用
synchronized
无法保证资源的互斥访问,可能会导致数据不一致等问题。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
为了实现分布式环境下的同步,可以使用以下几种常见的方案:
可以利用数据库的行级锁或表级锁来实现分布式锁。例如,在 MySQL 中,可以使用
SELECT ... FOR UPDATE
语句来获取行级锁。
-- 获取行级锁
SELECT * FROM distributed_lock_table WHERE lock_name = 'resource_lock' FOR UPDATE;
这种方式的优点是实现简单,不需要额外的组件;缺点是性能较差,对数据库的依赖较大。
Redis 是一个高性能的键值存储系统,可以利用 Redis 的原子操作来实现分布式锁。常见的实现方式是使用
SETNX(SET if Not eXists)
命令。
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private static final String LOCK_KEY = "distributed_lock";
private static final String LOCK_VALUE = "lock_value";
private static final int EXPIRE_TIME = 1000; // 锁的过期时间,单位:毫秒
public static boolean acquireLock(Jedis jedis) {
String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "PX", EXPIRE_TIME);
return "OK".equals(result);
}
public static void releaseLock(Jedis jedis) {
jedis.del(LOCK_KEY);
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
if (acquireLock(jedis)) {
try {
// 执行临界区代码
System.out.println("获取到锁,执行临界区代码");
} finally {
releaseLock(jedis);
}
} else {
System.out.println("未获取到锁");
}
jedis.close();
}
}
这种方式的优点是性能高,实现相对简单;缺点是需要额外的 Redis 服务,并且需要处理锁的过期时间和异常情况。
事实上,Redis 实现
TryLock
也要依靠 SET 命令的原子性,通过设置特定的键值对以及过期时间来模拟锁的获取和释放。当执行 SET 命令时,如果键不存在则设置成功,相当于获取到锁;若键已存在则设置失败,代表锁已被其他客户端持有。
以下是使用 Jedis实现
tryLock
功能的示例代码:
import redis.clients.jedis.Jedis;
public class RedisTryLockExample {
private static final String LOCK_KEY = "distributed_lock";
private static final String LOCK_VALUE = "lock_value";
private static final int EXPIRE_TIME = 10000; // 锁的过期时间,单位:毫秒
private Jedis jedis;
public RedisTryLockExample() {
this.jedis = new Jedis("localhost", 6379);
}
/**
* 尝试获取锁
* @return 如果获取到锁返回 true,否则返回 false
*/
public boolean tryLock() {
// 使用 SET 命令的 NX(Not eXists)和 PX(过期时间)选项
String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "PX", EXPIRE_TIME);
return "OK".equals(result);
}
/**