各种各样的锁的学习
前言
人是生而自由的,但却无往不在枷锁之中。自以为是其他一切的主人的人,反而比其他一切更是奴隶。 ——卢梭
一、十五把锁
序号 | 十五把锁 | 描述 |
---|---|---|
以 | 乐观锁 | 操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。 |
而 | 悲观锁 | 操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。 |
散 | 自旋锁 | 当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁,等待的机制被称为自旋锁。 |
思 | 重入锁 | 当一个线程成功获取到重入锁后,如果再次访问该临界区,该线程可以再次获取锁,而不会对自己产生互斥行为。 |
无 | 读写锁 | 一种允许多个读线程同时访问共享资源,但只允许一个写线程访问共享资源的锁机制。 |
溜 | 公平锁 | 多个线程按照申请锁的顺序来获取锁。 |
期 | 非公平锁 | 多个线程获取锁的顺序并不是按照申请锁的顺序。 |
吧 | 共享锁 | 当一个线程获取读锁时,其他线程可以同时获取读锁,但只有一个线程可以获取写锁。当其他线程都释放了读锁后,写锁才会被释放 |
酒 | 独占锁 | 只能被一个线程获取,并且只允许一个线程拥有该锁。当一个线程获取了独占锁时,其他线程必须等待该线程释放锁后才能获取该锁。 |
是 | 重量级锁 | 基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态和内核态之间切换,相对开销较大 |
是以 | 轻量级锁 | 在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(既互斥操作),如果同一时刻与多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。 |
是而 | 偏向锁 | 种针对多线程并发访问的锁机制,它主要解决的是在没有竞争的情况下如何提高性能的问题。让一个线程多次获得锁,减少其他线程获取锁的开销。有线程竞争的情况下,偏向锁会膨胀为轻量级锁,可能会导致性能下降。 |
是散 | 分段锁 | 将共享资源分割成多个段,并为每段都提供一把锁。解决多线程并发访问的锁机制,它主要解决的是在没有竞争的情况下如何提高性能的问题。 |
是思 | 互斥锁 | 只能被一个线程获取,并且只允许一个线程拥有该锁。当一个线程获取了互斥锁时,其他线程必须等待该线程释放锁后才能获取该锁。 |
是无 | 同步锁 | 保证在同一时刻只有一个线程可以执行某个代码块。当一个线程进入同步代码块时,它会获取该锁,执行完代码块后释放该锁。其他线程必须等待该线程释放锁后才能进入该代码块。 |
java里就是:
ReentrantLock,Synchronieed,ReentrantReadWriteLock,Atomic 全家桶,Concurrent全家桶
二、ReentrantLock
2.1 介绍
ReentrantLock是一个互斥的可重入锁。互斥的意思就是排他,独占,只能一个线程获取到锁。可重入的意思就是单个线程可以多次重复获取锁。
实现了悲观锁、重入锁、独占锁、非公平锁、互斥锁
2.2 常用方法:
方法名 | 说明 | 异常 |
---|---|---|
lock() | 一直阻塞获取锁,直到成功 | 无 |
lockInterruptibly() | 尝试获取锁,直到获取锁或者线程被中断 | InterruptedException |
tryLock() | 尝试获取空闲的锁,获取成功返回true,获取失败返回flase,不会阻塞,立即返回 | 无 |
tryLock(long time, TimeUnit unit) | 尝试在TIME时间内获取空闲的锁,在等待时间内可以被中断 | InterruptedException |
unLock() | 释放锁 | 无 |
newCondition() | 返回当前所得一个condition实例,可以唤醒或者等待线程 | 无 |
2.3 场景
递归嵌套的业务场景中,例如一棵树型的业务逻辑,方法有嵌套和调用,这时候我从外层加锁后,在递归遍历多次,每次都要是同一把锁,并且递归到其他层级时锁还不能失效。这个时候就可以使用重入锁了。江湖上还有个花名,叫递归锁。
三、Synchronized
3.1 介绍
Synchronized是悲观锁,默认是偏向锁,解锁失败后会升级为轻量级锁,当竞争继续加剧,进入CAS自旋10次后会升级为重量级锁。
实现了悲观锁、重入锁(用关键字修饰方法或代码段时)、独占锁,非公平锁、轻量级锁、重量级锁、偏向锁、同步锁
3.2 使用方法
3.2.1 修饰方法
1 | public synchronized void someMethod() { |
3.2.2 修饰代码块
1 | public void someMethod() { |
3.2.3 修饰静态方法
1 | public static synchronized void someStaticMethod() { |
3.2.4 修饰类
1 | public class MyClass { |
3.3 场景
多个线程访问共享变量,为了保证其原子性就可以使用 synchronized 。一个典型的业务场景是银行转账操作。假设有多个用户在同时进行转账操作,需要确保转账过程的原子性和数据的一致性,避免出现重复转账或者转账金额错误的情况。在这种情况下,可以使用 synchronized 关键字来实现同步访问。
四、ReentrantReadWriteLock
4.1 介绍
ReentrantReadWriteLock是Java提供的读写锁,它支持多个线程同时读取共享数据,但只允许一个线程进行写操作。
实现了读写锁、共享锁。
4.2 使用方法
方法 | 说明 |
---|---|
readLock() | 获取读锁 |
writeLock() | 获取写锁 |
readLock().lock() | 获取读锁并加锁 |
writeLock().lock() | 获取写锁并加锁 |
readLock().unLock() | 释放读锁 |
writeLock().unLock() | 释放写锁 |
newCondition() | 创建与锁关联的condition实例,可以唤醒或者等待线程 |
4.3 场景
读写锁应用在读多写少的情况下。读取时不涉及数据修改,写入时需要互斥操作。现在基本所有的号称效率高的准实时数据库都有实现读写锁的算法。
五、Atomic 全家桶
5.1 介绍
Atomic全家桶 我们以AtomicInteger为例。他的特点是实现了CAS算法,同时解决了ABA问题保证原子性。还实现了自旋锁的CLHLock算法,用于CAS比较失败后自旋等待。
它实现了乐观锁、自旋锁、轻量级锁
5.2 使用方法
方法 | 说明 |
---|---|
get() | 获取当前AtomicInteger对象的值 |
set(int newValue) | 将AtomicInteger对象的值设置为指定的新值 |
getAndSet(int newValue) | 将AtomicInteger对象的值设置为指定的新值,并返回旧值 |
incrementAndGet() | 将AtomicInteger对象的值**+**1,并返回递增后的新值 |
decrementAndGet() | 将AtomicInteger对象的值**-**1,并返回递减后的新值 |
getAndIncrement() | 先获取AtomicInteger对象的当前值,然后**+**1,并返回获取的旧值 |
getAndDecrement() | 先获取AtomicInteger对象的当前值,然后**-**1,并返回获取的旧值 |
addAndGet(int delta) | 将指定值加到AtomicInteger对象的值,返回相加后结果 |
getAndAdd(int delta) | 先获取AtomicInteger对象的当前值,然后将指定值加到AtomicInteger对象的值,返回获取的旧值 |
5.3 场景
用来做计数器非常合适,再有就是线程通讯,数据共享。
六、Concurrent全家桶
6.1 介绍
Concurrent全家桶我们以ConcurrentHashMap为代表。它实现了分段锁算法(Segmented Locking)的策略,将整个数据结构划分成多个Segments,每个段都拥有独立的锁。
6.2 使用方法
方法 | 说明 |
---|---|
clear() | 移除所有关系 |
containsKey(Object value) | 检查指定对象是否都为表中的健 |
containsValue(Object value) | 如果此映射将一个或多个键映射到指定值,返回true |
elements() | 返回此表值的枚举 |
entrySet() | 返回此映射所包含的映射关系Set视图 |
get() | 返回指定键映射到的值,如果此映射不包含该键的映射关系,则返回null |
isEmpty() | 此映射不包含键值则返回true |
keys() | 返回此表中健的枚举 |
put(K key, V value) | 指定将键映射到此表中的指定值 |
putAll(Map<? extends k, ? extends v> m) | 将指定映射中所有的映射关系复制到此映射中 |
size() | 返回此映射中的键值映射关系数 |
remove(Ob) | 将键从此映射中移除 |
replace(Kkey, V value) | 只有目前将键的条目映射到给定值时,才替换该键的条目 |
6.3 场景
在java中ConcurrentHashMap,就是将数据分为16段,每一段都有单独的锁,并且处于不同锁段的数据互不干扰,以此来提升锁的性能。
比如在秒杀扣库存的场景中,现在的库存中有2000个商品,用户可以秒杀。为了防止出现超卖的情况,通常情况下,可以对库存加锁。如果有1W的用户竞争同一把锁,显然系统吞吐量会非常低。为了提升系统性能,我们可以将库存分段,比如:分为100段,这样每段就有20个商品可以参与秒杀。在秒杀的过程中,先把用户id获取hash值,然后除以100取模。模为1的用户访问第1段库存,模为2的用户访问第2段库存,模为3的用户访问第3段库存,后面以此类推,到最后模为100的用户访问第100段库存。如此一来,在多线程环境中,可以大大地减少锁的冲突。
七、重点分布式场景redisson和ZK锁
7.1 Redisson
我们日常开发中用用的最多的场景还是分布式锁。提到分布式锁就不可回避Redisson。WHY?他就是权威好用。使用场景最多没有之一。Redisson官方一共提供了8把锁。
7.1.1 可重入锁(Reentrant Lock)
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
1 | public static void main(String[] args) throws InterruptedException { |
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断地延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
1 | // 加锁以后10秒钟自动解锁 |
Redisson同时还为分布式锁提供了异步执行的相关方法:
1 |
|
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.
7.1.2 公平锁(Fair Lock)
基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
1 |
|
同样也有看门狗机制来防止死锁。另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
1 | // 10秒钟以后自动解锁 |
Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:
1 | RLock fairLock = redisson.getFairLock("anyLock"); |
7.1.3 联锁(MultiLock)
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
1 | public static void main(String[] args) throws InterruptedException { |
同样也有看门狗机制来防止死锁。另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
1 | RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3); |
7.1.4 红锁(RedLock)
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
1 | RLock lock1 = redissonInstance1.getLock("lock1"); |
1 | public static void main(String[] args) throws InterruptedException { |
7.1.5 读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
1 |
|
7.1.6 信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
1 | public static void main(String[] args) throws InterruptedException { |
7.1.7 可过期性信号量(PermitExpirableSemaphore)
基于Redis的Redisson可过期性信号量(PermitExpirableSemaphore)是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。它提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
1 | public static void main(String[] args) throws InterruptedException { |
7.1.8 闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
1 | public static void main(String[] args) throws InterruptedException { |
7.2 Zookeeper
接着我们再看zookeeper的锁。zk不是为高可用性设计的,但它使用ZAB协议达到了极高的一致性。所以它经常被选作注册中心、配置中心、分布式锁等场景。它的性能是非常有限的,而且API并不是那么好用。xjjdog倾向于使用基于Raft协议的Etcd或者Consul,它们更加轻量级一些。我们看一下zk的加锁时序图。
Curator是netflix公司开源的一套zookeeper客户端,目前是Apache的顶级项目。与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。Curator解决了很多zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册wathcer和NodeExistsException 异常等。Curator由一系列的模块构成,对于一般开发者而言,常用的是curator-framework和curator-recipes,我们跳过他的其他能力,直接看分布式锁。
7.2.1 可重入锁(Shared Reentrant Lock)
Shared意味着锁是全局可见的, 客户端都可以请求锁。Reentrant和JDK的ReentrantLock类似, 意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类InterProcessMutex来实现。通过acquire获得锁,并提供超时机制。通过release()方法释放锁。InterProcessMutex 实例可以重用。Revoking ZooKeeper recipes wiki定义了可协商的撤销机制。为了撤销mutex, 调用makeRevocable方法。我们来看示例:
1 |
|
7.2.2 不可重入锁(Shared Lock)
使用InterProcessSemaphoreMutex,调用方法类似,区别在于该锁是不可重入的,在同一个线程中不可重入。
7.2.3 可重入读写锁(Shared Reentrant Read Write Lock)
类似JDK的ReentrantReadWriteLock. 一个读写锁管理一对相关的锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁使用时不允许读 (阻塞)。此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁, 比如请求写锁 —>读锁 —->释放写锁。从读锁升级成写锁是不成的。主要由两个类实现:
1 | InterProcessReadWriteLock |
7.2.4 信号量(Shared Semaphore)
一个计数的信号量类似JDK的Semaphore。JDK中Semaphore维护的一组许可(permits),而Cubator中称之为租约(Lease)。注意,所有的实例必须使用相同的numberOfLeases值。调用acquire会返回一个租约对象。客户端必须在finally中close这些租约对象,否则这些租约会丢失掉。但是, 但是,如果客户端session由于某种原因比如crash丢掉, 那么这些客户端持有的租约会自动close, 这样其它客户端可以继续使用这些租约。租约还可以通过下面的方式返还:
1 | public void returnAll(Collection<Lease> leases) |
注意一次你可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。同时还提供了超时的重载方法:
1 | public Lease acquire() |
主要类有:
1 | InterProcessSemaphoreV2 |
7.2.5 多锁对象(Multi Shared Lock)
Multi Shared Lock是一个锁的容器。当调用acquire, 所有的锁都会被acquire,如果请求失败,所有的锁都会被release。同样调用release时所有的锁都被release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。主要涉及两个类:
1 | InterProcessMultiLock |
它的构造函数需要包含的锁的集合,或者一组ZooKeeper的path。
1 | public InterProcessMultiLock(List<InterProcessLock> locks) |
7.2.6 完整锁示例
1 |
|
八、总结
分布式环境中,我们始终绕不开CAP理论,这也是Redisson锁和ZK锁的本质区别。
CAP指的是在一个分布式系统中:
一致性(Consistency)
可用性(Availability)
分区容错性(Partition tolerance)
这三个要素最多只能同时实现两点,不可能三者兼顾。如果你的实际业务场景,更需要的是保证数据一致性。那么请使用CP类型的分布式锁,比如:zookeeper,它是基于磁盘的,性能可能没那么好,但数据一般不会丢。
如果你的实际业务场景,更需要的是保证数据高可用性。那么请使用AP类型的分布式锁,比如:Redisson,它是基于内存的,性能比较好,但有丢失数据的风险。
其实,在我们绝大多数分布式业务场景中,使用Redisson分布式锁就够了,真的别太较真。因为数据不一致问题,可以通过最终一致性方案解决。但如果系统不可用了,对用户来说是暴击一万点伤害。
九、思考
以上就是我对锁的总结。分布式锁不是完美的,总会有问题;如:在redis中,lockName和已有的key不能重名 !unlock的前提是lock成功!必须要设计自己的兜底方案……
1 | 本文为个人知识学习,非原创!非作者!如本博客有侵权行为,请与我联系。 |