⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/qq_42505148 「咸鱼睡不醒_」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

首先说结论,在分布式系统中,单纯使用 Java 中的 synchronized 关键字是无法满足需求的,下面从 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 修饰,确保了在同一时刻只有一个线程能够执行该方法,从而避免了多线程环境下的竞态条件。

二、在分布式场景下的局限性

分布式系统由多个独立的 JVM 进程组成,不同进程之间无法直接共享对象的监视器。synchronized 只能保证单个 JVM 内的线程同步,无法实现跨 JVM 进程的同步。

因此,在分布式系统中,如果多个进程同时访问共享资源,使用 synchronized 无法保证资源的互斥访问,可能会导致数据不一致等问题。

三、分布式系统中的替代方案

为了实现分布式环境下的同步,可以使用以下几种常见的方案:

1.数据库锁

可以利用数据库的行级锁或表级锁来实现分布式锁。例如,在 MySQL 中,可以使用 SELECT ... FOR UPDATE 语句来获取行级锁。

-- 获取行级锁
SELECT * FROM distributed_lock_table WHERE lock_name = 'resource_lock' FOR UPDATE;

这种方式的优点是实现简单,不需要额外的组件;缺点是性能较差,对数据库的依赖较大。

2.Redis 分布式锁——SETNX

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 服务,并且需要处理锁的过期时间和异常情况。

3.Redis 分布式锁——TryLock

事实上,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);
}

/**
* 释放锁
*/
public void unlock() {
jedis.del(LOCK_KEY);
}

public static void main(String[] args) {
RedisTryLockExample lockExample = new RedisTryLockExample();
if (lockExample.tryLock()) {
try {
// 模拟执行临界区代码
System.out.println("获取到锁,执行临界区代码");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockExample.unlock();
System.out.println("释放锁");
}
} else {
System.out.println("未获取到锁");
}
lockExample.jedis.close();
}
}

比较SETNX和TryLock:

  • SETNX 的问题:

使用 SETNX 命令设置锁时,无法原子性地设置锁的过期时间。若在设置完锁之后,在设置过期时间之前,客户端发生异常崩溃,就会导致锁无法释放,造成死锁。

SETNX只是单纯地判断键是否存在并进行设置,对于锁的过期时间等额外属性需要额外的命令来处理,在实现分布式锁时需要编写更多的代码来保证正确性,代码的可读性和可维护性较差。功能相对单一,仅能实现键的存在性判断和设置操作。

  • TryLock 实现的优势:

使用 SET 命令并结合 NX 和 PX/EX 选项可以原子性地完成锁的设置和过期时间的设置,避免了上述死锁问题。从功能语义上来说,TryLock 更符合锁的使用习惯,调用者可以直观地理解其作用是尝试获取锁,并且能够方便地结合过期时间等参数进行使用,代码逻辑更加清晰。

TryLock基于 SET 命令可以利用 Redis 提供的更多特性,例如可以设置不同的过期时间单位(PX 表示毫秒,EX 表示秒),还可以结合其他命令实现更复杂的分布式锁逻辑,如可重入锁等。

四、总结

综上所述,在分布式系统中不能直接使用 synchronized 来实现同步,需要根据具体的业务场景选择合适的分布式锁方案。

文章目录
  1. 1. 一、synchronized 的作用原理
  2. 2. 二、在分布式场景下的局限性
  3. 3. 三、分布式系统中的替代方案
    1. 3.1. 1.数据库锁
    2. 3.2. 2.Redis 分布式锁——SETNX
    3. 3.3. 3.Redis 分布式锁——TryLock
      1. 3.3.1. 比较SETNX和TryLock:
  4. 4. 四、总结