面试题总结-锁
乐观锁/悲观锁
乐观锁与悲观锁并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。
乐观锁
- 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。
- 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用“数据版本机制”或“CAS操作”来实现。
数据版本机制
实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳。以版本号方式为例。
版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL代码:
1 update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
CAS操作
CAS(Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
悲观锁
- 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。
- 悲观锁在Java中的使用,就是利用各种锁。
在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
独享锁/ 共享锁
独享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁
- 互斥锁在Java中的具体实现就是ReentrantLock。
读写锁
- 读写锁在Java中的具体实现就是ReadWriteLock。
可重入锁
- 什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
可重入锁有:
- synchronized
- ReentrantLock
公平锁
非公平锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
Redlock红锁
Zookeeper实现分布式锁
基于临时顺序节点实现(推荐)
锁分为两种:共享锁(读锁)和排他锁(写锁)
读锁:当有一个线程获取读锁后,其他线程也可以获取读锁,但是在读锁没有完全被释放之前,其他线程不能获取写锁。
写锁:当有一个线程获取写锁后,其他线程就无法获取读锁和写锁了。
zookeeper有一种节点类型叫做临时序号节点,它会按序号自增地创建临时节点,这正好可以作为分布式锁的实现工具。
读锁获取原理:
1、根据资源的id创建临时序号节点:/lock/mylockR0000000005 Read
2、获取/lock下的所有子节点,判断比他小的节点是否全是读锁,如果是读锁则获取锁成功
3、如果不是,则阻塞等待,监听自己的前一个节点。
4、当前面一个节点发生变更时,重新执行第二步操作。
写锁获取原理:
1、根据资源的id创建临时序号节点:/lock/mylockW0000000006 Write
2、获取 /lock 下所有子节点,判断最小的节点是否为自己,如果是则获锁成功
3、如果不是,则阻塞等待,监听自己的前一个节点
4、当前面一个节点发生变更时,重新执行第二步。
Redis分布式锁
Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁;
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);
最低保证分布式锁的有效性及安全性的要求如下:
- 互斥;任何时刻只能有一个client获取锁
- 释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁
1.加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
值得注意的是:
- random_value 是客户端生成的唯一的字符串。
- NX 代表只在键不存在时,才对键进行设置操作。
- PX 5000 设置键的过期时间为5000毫秒。
这样,如果上面的命令执行成功,则证明客户端获取到了锁。
2.解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。
使用lua脚本执行,首先判断randon_value是否一致,一致则删除。