Java常用锁机制

乐观锁

乐观锁并不是一种具体的锁机制,而是一种并发控制策略。

乐观锁比较乐观,他认为没有人会和他抢着修改数据。因此乐观锁不会上锁,只有在提交的时候才会对数据是否冲突进行检测。如果存在冲突则会抛出一个异常(如OptimisticLockException),提示当前操作存在冲突。即,可以选择回滚当前事务实现数据准确性。

乐观锁允许多个事务或线程同时读取数据,而不会相互阻塞。

核心思想:

在数据更新时检查数据的版本号或时间戳,如果版本号或时间戳与读取时的一致,则说明数据没有被其他事务修改,可以进行更新;如果不一致,则说明数据已经被其他事务修改,此时会触发冲突。

在使用上:

乐观锁在Java中的使用是无锁编程,常常采用的是CAS机制、fail-fast等

MySQL数据库中的应用

1
update 表 set ... ,version = version +1 where id = #{id} and version = #{version}

操作前,先读取记录的版本号,更新时,通过SQL语句比较版本号是否一致。如果一致,则更新数据。否则会再次读取版本,重试上面的操作

使用场景:

解决超卖问题、保证库存准确性

示例——MyBatisPlus乐观锁实现借阅图书

1.创建图书库存表

1
2
3
4
5
6
7
8
##mysql
CREATE TABLE BookInventory (
`id` bigint(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`version` int(255) DEFAULT '1',
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.注册乐观锁配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加具体的拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//3.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}

3.实体类

1
2
3
4
5
6
7
8
9
@Data
@TableName("BookInventory")
public class BookInventory {
private Long id;
private String name;
@Version
private Integer version;
private Integer num;
}

@Version注解标示这个字段为版本字段,一般为Integer或Long类型,在更新数据时,MyBatis-Plus 会自动对该字段的值进行检查和递增

4.具体实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** controller */
@RestController
@RequestMapping("/lock")
public class OptimisticLockController {

@Autowired
private OptimisticLockService optimisticLockService;

@GetMapping("/optimistic")
public String optimisticLock() {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new ThreadImpl(optimisticLockService));
thread.start();
thread.setName("线程"+i);
}
return "Optimistic Lock Success";
}
}

//=====================================================

/** serviceImpl **/
public class ThreadImpl implements Runnable {

public OptimisticLockService optimisticLockService;

public ThreadImpl(OptimisticLockService optimisticLockService) {
this.optimisticLockService = optimisticLockService;
}
static Lock lock = new ReentrantLock();

@Override
public void run() {
//lock.lock();
BookInventory byId = optimisticLockService.getById(1);// 读取库存数据
if(byId.getNum() != 0){
int i = byId.getNum() - 1;
byId.setNum(i);
boolean result = optimisticLockService.updateById(byId); // 执行更新

if (!result) {
// 如果返回值为 0,说明更新失败,版本号不匹配
System.out.println("更新失败,数据可能已经被其他用户修改");
} else {
// 更新成功,MyBatis-Plus 会自动将 version 字段加 1
System.out.println("更新成功");
}
String name = Thread.currentThread().getName();
System.out.println(name+"-"+i);
//lock.onlock();
}
}
}

5.运行时返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
更新失败,数据可能已经被其他用户修改
更新成功
线程7-8
更新失败,数据可能已经被其他用户修改
线程4-8
更新失败,数据可能已经被其他用户修改
更新失败,数据可能已经被其他用户修改
线程5-8
线程2-8
线程6-8
更新失败,数据可能已经被其他用户修改
线程9-8
更新失败,数据可能已经被其他用户修改
线程1-8
更新失败,数据可能已经被其他用户修改
更新失败,数据可能已经被其他用户修改
线程8-8
线程3-8
更新失败,数据可能已经被其他用户修改
线程0-8

同时十个线程请求,只会有一个显示更新成功

示例——原子操作类

支持在多线程环境下对整数进行操作

1
2
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();

AtomicInteger使用详解

  1. get():获取当前AtomicInteger对象的值。
  2. set(int newValue):将AtomicInteger对象的值设置为指定的newValue。
  3. getAndSet(int newValue):先获取当前AtomicInteger对象的值,然后将对象的值设置为指定的newValue。
  4. compareAndSet(int expect, int update):比较当前对象的值与expect是否相等,如果相等,则将对象的值设置为update。
  5. getAndIncrement():获取当前AtomicInteger对象的值,并将对象的值加1。
  6. getAndDecrement():获取当前AtomicInteger对象的值,并将对象的值减1。
  7. incrementAndGet():将当前AtomicInteger对象的值加1,并返回增加后的值。
  8. decrementAndGet():将当前AtomicInteger对象的值减1,并返回减少后的值。
  9. addAndGet(int delta):将指定的delta值加到当前AtomicInteger对象的值上,并返回增加后的值。
  10. getAndAdd(int delta):获取当前AtomicInteger对象的值,并将指定的delta值加到对象的值上。

附:CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的同步。
CAS算法涉及到三个操作数:当前内存值 V、原始值 A、要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。

优点‌:

  1. 减少锁的使用‌:乐观锁通过版本控制来避免数据冲突,减少了锁的使用,提高了并发性能‌。
  2. 减少系统开销‌:不需要频繁地加锁和解锁,从而减少了系统开销‌。
  3. 适用于读多写少的场景‌:在读多写少的场景下,乐观锁能够减少锁争用,提高系统性能‌。

缺点‌:

  1. 可能需要重试‌:当数据版本不一致时,需要回滚事务并重新尝试,这可能会增加系统负担‌。
  2. 无法防止写冲突‌:在高并发写操作的场景下,乐观锁可能导致更多的冲突和资源浪费‌。
  3. 版本维护‌:需要维护版本信息,占用额外的存储空间‌。

悲观锁

悲观锁比较悲观,他认为在他操作数据的时候肯定会有人和他一起抢着修改数据。所以在操作数据的时候会把数据锁住,其他的线程无法查询修改删除当前数据,必须等到锁释放之后才能继续操作。

核心思想:

在操作数据时,会把数据锁住,直到操作完成。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。

在使用上:

可以采用 JAVA 自带的 synchronized 关键字,通过添加到方法或同步块上,锁住资源。 如果是分布式系统,我们可以借助数据库自身的锁机制来实现。

1
select * from 表名 where id = #{id} for update;

注意:使用悲观锁的时候,我们要注意锁的级别,MySQL innodb 在加锁时,只有明确的指定主键或(索引字段)才会使用 行锁;否则,会执行 表锁,将整个表锁住,此时性能会很差。在使用悲观锁时,我们必须关闭 MySQL 数据库的自动提交属性,因为mysql默认使用自动提交模式。悲观锁适用于写多的场景,而且并发性能要求不高

使用场景:

银行系统扣款