演示synchronized锁机制用法的简单Demo
演示synchronized
锁机制用法的简单Demo。我们以"银行开户"场景为例:每个用户只能创建一个账户(模拟类似原代码中每个用户只能有一个私有空间的限制)。
第1步:创建项目结构
demo-lock
├── src/main/java/com/example/demo/
│ ├── controller/AccountController.java
│ ├── entity/Account.java
│ ├── mapper/AccountMapper.java
│ ├── service/AccountService.java
│ └── DemoApplication.java
└── src/main/resources/
└── application.yml
第2步:添加依赖(pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
第3步:实体类 Account.java
@Data
@TableName("t_account")
public class Account {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private BigDecimal balance;
}
第4步:Mapper接口 AccountMapper.java
public interface AccountMapper extends BaseMapper<Account> {
}
第5步:Service层 AccountService.java
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountMapper accountMapper;
// 无锁版本(存在并发问题)
public void createAccountUnsafe(Long userId) {
Long count = accountMapper.selectCount(new QueryWrapper<Account>().eq("user_id", userId));
if (count > 0) {
throw new RuntimeException("用户已存在账户");
}
Account account = new Account();
account.setUserId(userId);
account.setBalance(BigDecimal.ZERO);
accountMapper.insert(account);
}
// 有锁版本(线程安全)
public void createAccountWithLock(Long userId) {
String lockKey = String.valueOf(userId).intern();
synchronized (lockKey) {
createAccountUnsafe(userId);
}
}
}
第6步:Controller层 AccountController.java
@RestController
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;
// 不安全的开户接口(用于演示并发问题)
@GetMapping("/unsafe/{userId}")
public String unsafeCreate(@PathVariable Long userId) {
try {
accountService.createAccountUnsafe(userId);
return "开户成功";
} catch (Exception e) {
return e.getMessage();
}
}
// 安全的开户接口(使用synchronized锁)
@GetMapping("/safe/{userId}")
public String safeCreate(@PathVariable Long userId) {
try {
accountService.createAccountWithLock(userId);
return "开户成功";
} catch (Exception e) {
return e.getMessage();
}
}
}
第7步:配置文件 application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&characterEncoding=utf8
username: root
password: your_password
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
第8步:测试步骤
- 初始化数据库
CREATE DATABASE IF NOT EXISTS demo;
USE demo;
CREATE TABLE t_account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL UNIQUE,
balance DECIMAL(10,2) NOT NULL DEFAULT 0
);
ALTER TABLE t_account DROP INDEX user_id;
- 启动应用
mvn spring-boot:run
- 并发测试(使用JMeter或Postman)
测试不安全接口:
- 用多个线程同时调用
GET http://localhost:8080/unsafe/123
- 可能结果:成功创建多个账户(违反唯一约束)
测试安全接口:
- 用多个线程同时调用
GET http://localhost:8080/safe/456
- 结果:只会有第一个请求成功创建账户
关键代码解释
- 锁对象的选择:
String lockKey = String.valueOf(userId).intern();
intern()
保证相同userId值返回同一个String对象(来自字符串常量池)- 不同userId对应的锁对象不同,实现细粒度锁
- 同步代码块:
synchronized (lockKey) {
// 临界区代码
}
- 确保同一用户的并发请求串行执行
- 不同用户的请求可以并行处理
典型输出对比
无锁接口测试结果:
第一次请求:开户成功(创建账户)
第二次请求:Duplicate entry '123' for key 'user_id'(违反唯一约束)
有锁接口测试结果:
第一次请求:开户成功(创建账户)
后续所有请求:用户已存在账户(业务校验拦截)
总结说明表格
关键点 | 说明 |
---|---|
synchronized范围 | 基于用户ID的细粒度锁,不影响其他用户操作 |
String.intern() | 保证相同userid得到的String是同一个对象(来自字符串常量池) |
事务边界 | 在锁范围内包含整个事务操作(确保查询和插入操作的原子性) |
性能影响 | 只对相同用户的并发请求串行化处理,不影响不同用户的并发处理 |
适用场景 | 需要基于特定维度(如用户ID)进行并发控制的场景 |
可以通过这个Demo逐步体验:
- 先观察不加锁时的并发问题
- 再体验加锁后的线程安全效果
- 最后尝试调整userId观察不同用户的并发情况
Jmeter测试
-
设置 HTTP 请求
注意:这里让多个线程同时使用相同的 userId。/unsafe/1
,这样所有线程都会尝试为同一个用户(userId=1)创建账户。 -
设置线程组
-
添加 查看结果树
-
运行
-
数据库结果
-
加了锁的
无论 并发请求是多少,在关闭数据库的唯一约束的情况下,数据库插入条数始终为1。
-
没有加锁
产生了多条插入记录,显然是不合理的。