什么时候用synchronized?什么时候用分布式锁?
前言
最近在做项目的时候,遇到一个并发问题,当时一门心思的想着要加锁,一开始加的是分布式锁,后来测试过程中发现这个锁加的太重了,也没实现自己的要求,就开始思考是不是方向有问题了,于是又来复习下这方面的内容。
本地资源和外部数据源
-
本地资源和外部数据源的基本概念
- 本地资源:在单 JVM 环境下,资源如果是在应用程序自己的内存空间(如堆内存中的对象、方法区中的静态变量)或者本地文件系统中(应用程序所在服务器的本地硬盘上的文件),这些都属于本地资源。例如,一个简单的
ArrayList
对象存储在堆内存中,被应用程序内部的方法使用,这就是本地资源。 - 外部数据源:外部数据源是指存储在应用程序所在 JVM 之外的存储系统中的数据。这些存储系统可以通过网络进行访问,并且通常是为多个应用程序(可能是多个 JVM)提供数据存储和读取服务的。
- 本地资源:在单 JVM 环境下,资源如果是在应用程序自己的内存空间(如堆内存中的对象、方法区中的静态变量)或者本地文件系统中(应用程序所在服务器的本地硬盘上的文件),这些都属于本地资源。例如,一个简单的
-
常见的外部数据源类型及识别方法
- 分布式数据库
- 连接方式:如果你的应用程序通过网络连接(如使用 JDBC 驱动,并配置了数据库服务器的 IP 地址、端口号、用户名和密码)来访问数据库(如 MySQL Cluster、Oracle RAC 等),那么这个数据库就是外部数据源。例如,在配置文件中有类似
jdbc:mysql://database - server - ip:3306/mydatabase
这样的数据库连接字符串,就表明是在访问外部的数据库服务器。 - 多应用共享特征:多个不同的应用程序(不同的 JVM)可以连接到同一个分布式数据库,并且可以对数据库中的表进行读写操作。例如,一个电商系统中的用户服务和订单服务都连接到同一个 MySQL 数据库,它们通过不同的数据库表(如用户表和订单表)来存储和获取数据,这个 MySQL 数据库就是外部数据源。
- 连接方式:如果你的应用程序通过网络连接(如使用 JDBC 驱动,并配置了数据库服务器的 IP 地址、端口号、用户名和密码)来访问数据库(如 MySQL Cluster、Oracle RAC 等),那么这个数据库就是外部数据源。例如,在配置文件中有类似
- 分布式缓存(如 Redis、Memcached)
- 连接与配置:当你通过特定的客户端库(如 Jedis 用于 Redis)和配置信息(包括服务器地址、端口号等)来连接缓存系统时,这就是外部数据源。例如,在代码中有
Jedis jedis = new Jedis("redis - server - ip", 6379);
这样的代码,就表明是在连接外部的 Redis 缓存服务器。 - 共享缓存数据:多个应用程序(不同的 JVM)可以向缓存中存储数据和获取数据。比如,多个微服务可以将一些经常访问的数据(如用户登录信息、商品热门列表等)存储在 Redis 缓存中,并且可以共享这些缓存数据,这种情况下 Redis 就是外部数据源。
- 连接与配置:当你通过特定的客户端库(如 Jedis 用于 Redis)和配置信息(包括服务器地址、端口号等)来连接缓存系统时,这就是外部数据源。例如,在代码中有
- 分布式文件系统(如 Ceph、HDFS)
- 访问接口与协议:如果应用程序通过特定的文件系统客户端库和网络协议(如 Ceph 的 librados 库、HDFS 的 Java API)来访问文件系统中的文件,那么这个文件系统就是外部数据源。例如,在代码中有
FileSystem fs = FileSystem.get(new Configuration());
(HDFS 的 Java API)这样的代码,并且配置指向了远程的 HDFS 服务器,就表明是在访问外部的分布式文件系统。 - 多应用共享文件资源:多个不同的应用程序(不同的 JVM)可以对分布式文件系统中的文件进行读写操作。例如,一个大数据处理系统中的多个数据处理任务(分布在不同的 JVM)可以从 HDFS 中读取数据文件,并将处理后的结果写回到 HDFS 中的其他文件,这里的 HDFS 就是外部数据源。
- 访问接口与协议:如果应用程序通过特定的文件系统客户端库和网络协议(如 Ceph 的 librados 库、HDFS 的 Java API)来访问文件系统中的文件,那么这个文件系统就是外部数据源。例如,在代码中有
- 消息队列(如 RabbitMQ、Kafka)
- 连接与消息发送 / 接收机制:当应用程序通过特定的客户端库(如 RabbitMQ 的 Java 客户端、Kafka 的 Java 客户端)和配置信息(包括服务器地址、端口号、队列名称等)来连接消息队列,并通过发送和接收消息来进行通信时,消息队列就是外部数据源。例如,在代码中有
ConnectionFactory factory = new ConnectionFactory(); factory.setHost("mq - server - ip");
(RabbitMQ 的 Java 客户端)这样的代码,就表明是在连接外部的消息队列服务器。 - 多应用消息交互:多个不同的应用程序(不同的 JVM)可以通过消息队列进行消息传递。比如,一个订单服务通过消息队列向库存管理服务发送库存更新消息,库存管理服务接收这些消息并进行库存处理,这里的消息队列就是外部数据源。
- 连接与消息发送 / 接收机制:当应用程序通过特定的客户端库(如 RabbitMQ 的 Java 客户端、Kafka 的 Java 客户端)和配置信息(包括服务器地址、端口号、队列名称等)来连接消息队列,并通过发送和接收消息来进行通信时,消息队列就是外部数据源。例如,在代码中有
- 分布式数据库
什么时候用synchronized?什么时候用分布式锁?
-
synchronized 的适用情况
- 单机环境简单并发场景:如果你的应用是在一台机器上运行,并且只是要防止同一个 Java 程序(JVM)里的多个线程同时访问某个共享资源,比如一个本地的缓存数据结构,或者一个本地文件的读写操作,就可以用 synchronized。它就像是给你家里(JVM)的某个房间(共享资源)上了一把锁,只有拿到这把钥匙(锁)的家庭成员(线程)才能进入这个房间。
- 资源访问频率不高的情况:当对共享资源的访问不是很频繁,而且你可以接受一定程度的性能损耗(因为 synchronized 相对比较 “重”,会有一些性能开销)。例如一个小型工具类中的方法,偶尔会被多个线程调用,使用 synchronized 可以简单地保证方法执行的互斥性。
-
分布式锁的适用情况
- 分布式系统场景:当你的应用是分布在多台机器上,像一个大型的电商系统,订单服务部署在多个服务器上。这些服务器上的程序可能会同时操作一些共享资源,如全局库存数量。这时候单机的 synchronized 就不管用了,因为它只能控制一台机器上的线程,需要分布式锁来协调不同机器上的程序对共享资源的访问,就好像在多栋楼(不同服务器)之间共享一个仓库(共享资源),需要一种能跨楼控制仓库访问的锁。
- 高并发且数据一致性要求极高的场景:在高并发环境下,对共享资源的操作必须保证严格的一致性,如金融系统中的账户余额操作。分布式锁可以确保在大量并发请求时,只有一个请求能够修改账户余额,避免数据混乱,就像银行(高并发系统)里多个柜员(不同的服务器请求)操作同一个账户(共享资源)时,需要一种严格的锁机制来保证账户余额的准确。
实例介绍
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
// 定义一个枚举类型表示游戏角色的职业
enum GameRoleClassEnum {
WARRIOR, MAGE, ROGUE, PRIEST
}
// 定义游戏角色配置类
class GameRoleConfiguration {
private int health;
private int attack;
private int defense;
private GameRoleClassEnum roleClass;
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
public int getAttack() {
return attack;
}
public void setAttack(int attack) {
this.attack = attack;
}
public int getDefense() {
return defense;
}
public void setDefense(int defense) {
this.defense = defense;
}
public GameRoleClassEnum getRoleClass() {
return roleClass;
}
public void setRoleClass(GameRoleClassEnum roleClass) {
this.roleClass = roleClass;
}
}
// 定义游戏角色配置工厂类
class GameRoleConfigFactory {
public static GameRoleConfiguration getGameRoleConfigByType(Integer type) {
GameRoleConfiguration config = new GameRoleConfiguration();
switch (type) {
case 1:
config.setRoleClass(GameRoleClassEnum.WARRIOR);
config.setHealth(100);
config.setAttack(20);
config.setDefense(15);
break;
case 2:
config.setRoleClass(GameRoleClassEnum.MAGE);
config.setHealth(60);
config.setAttack(30);
config.setDefense(5);
break;
case 3:
config.setRoleClass(GameRoleClassEnum.ROGUE);
config.setHealth(70);
config.setAttack(25);
config.setDefense(8);
break;
case 4:
config.setRoleClass(GameRoleClassEnum.PRIEST);
config.setHealth(80);
config.setAttack(10);
config.setDefense(12);
break;
default:
break;
}
return config;
}
}
// 游戏角色配置工具类
public class GameRoleUtil {
private static final int DEFAULT_HEALTH = 50;
private static final int DEFAULT_ATTACK = 10;
private static final int DEFAULT_DEFENSE = 8;
private static final Map<Integer, Function<Integer, GameRoleConfiguration>> roleConfigFunctionMap = new HashMap<>();
static {
// 为不同类型的游戏角色配置注册对应的创建函数
roleConfigFunctionMap.put(1, GameRoleConfigFactory::getGameRoleConfigByType);
roleConfigFunctionMap.put(2, GameRoleConfigFactory::getGameRoleConfigByType);
roleConfigFunctionMap.put(3, GameRoleConfigFactory::getGameRoleConfigByType);
roleConfigFunctionMap.put(4, GameRoleConfigFactory::getGameRoleConfigByType);
}
/**
* 生成游戏角色配置
*
* @param type 角色类型
* @param health 生命值
* @param attack 攻击力
* @param defense 防御力
* @return 生成的游戏角色配置
*/
public static GameRoleConfiguration generateGameRoleConfig(Integer type, Integer health, Integer attack, Integer defense) {
Function<Integer, GameRoleConfiguration> generateFunction = roleConfigFunctionMap.get(type);
GameRoleConfiguration roleConfig = generateFunction.apply(type);
health = health == null? DEFAULT_HEALTH : health;
attack = attack == null? DEFAULT_ATTACK : attack;
defense = defense == null? DEFAULT_DEFENSE : defense;
roleConfig.setHealth(health);
roleConfig.setAttack(attack);
roleConfig.setDefense(defense);
return roleConfig;
}
}
总结
写着写着突然发现有些基本功丢失了,常拾常新。