Java中锁分类.jpg

基于锁的获取与释放方式分类

隐式锁(内置锁或自动锁)

通过synchronized关键字实现的一种线程同步机制,当一个线程进入被synchronized修饰的方法或代码块时,它会自动获得对象级别的锁,退出该方法或代码块时则会自动释放这把锁。

隐式锁的实现机制主要包括以下两种类型

1.互斥锁 
  java虚拟机对synchronized 关键字处理的底层实现中,当锁竞争激烈且必须升级重量级锁时,会利用操作系统互斥量机制确保在同一时刻仅允许一个线程持有锁,从而实现严格的线程互斥控制
2.内部锁或监视器锁
  通过使用synchronized关键字便捷地管理和操作相应对象的监视器锁并在执行完毕后自动释放,这一过程对用户透明,故被称为隐式锁。每个Java对象均与一个监视器锁关联,以此来协调对该对象状态访问的并发控制

隐式锁适用场景

- 简洁易用,确保了线程安全,避免了竞态条件,异常处理下自动释放.
- 锁定颗粒通常是对象级别,相对显示锁不灵活,高并发下可能引起锁争用,导致性能下级.
- 适用于相对简单的多线程同步需求,尤其是在只需要保护某个对象状态完整性,且无需过多关注锁策略灵活性的场合。对于要求更高并发性和更复杂锁管理逻辑的应用场景,显示锁通常是一个更好的选择。

显式锁

1. 显式锁是由java.util.concurrent.locks.Lock接口及其诸多实现类提供的同步机制
2. 显式锁赋予开发者更为精细和灵活的控制能力,使其能够在多线程环境中精准掌控同步动作。
3. 显式锁的核心作用在于确保在任何时刻仅有一个线程能够访问关键代码段或共享数据,从而有效防止数据不一致性问题和竞态条件

常见的显式锁

1. ReentrantLock:可重入锁,继承自Lock接口,支持可中断锁、公平锁和非公平锁的选择。可重入意味着同一个线程可以多次获取同一线程持有的锁。
2. ReentrantReadWriteLock:读写锁,提供了两个锁,一个是读锁,允许多个线程同时读取;另一个是写锁,同一时间内只允许一个线程写入,写锁会排斥所有读锁和写锁。
3. StampedLock:带版本戳的锁,提供了乐观读、悲观读写模式,适合于读多写少的场景,可以提升系统性能。

显式锁适用场景

- 显式锁提供了多种获取和释放锁的方式 如中断等待锁的线程,设置超时获取锁等,在某些特定场景下,显式锁可以提供比隐式锁更好的性能表现,尤其是当需要避免死锁或优化读多写少的情况时。显式锁允许创建公平锁,按照线程请求锁的顺序给予响应,保证所有线程在等待锁时有一定的公平性。
- 使用复杂,需要手动调用lock()和unlock(),额外API调用和锁状态管理可能带来额外的性能开销,操作更加细致,因此更容易出错
- 显式锁适用于复杂的事务控制和并发访问场景,例如需要精确控制锁的粒度和范围,或者需要实现特定的并发控制策略。

基于对资源的访问权限

独占锁(Exclusive Lock) |排他锁|写锁

- 独占锁主要用于保护那些在并发环境下会被多个线程修改的共享资源,确保在修改期间不会有其他线程干扰,从而维护数据的一致性和完整性。
- 独占锁的实现方式,主要有如下两种:
    1.synchronized关键字:通过synchronized关键字实现的隐式锁,它是独占锁的一种常见形式,任何时刻只有一个线程可以进入被synchronized修饰的方法或代码块。
    2.ReentrantLock:可重入的独占锁,提供了更多的控制方式,包括可中断锁、公平锁和非公平锁等。
- 优点: 简单易用,线程安全,可重入性
- 缺点: 粒度固定,缺乏灵活性,性能瓶颈

共享锁(Shared Lock)| 读锁(Read Lock)

- 允许多个线程同时读取共享资源,但不允许任何线程修改资源。在数据库系统和并发编程中广泛使用,确保在并发读取场景下数据的一致性
- 实现共享锁的关键机制是读写锁(ReadWriteLock)
   ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
   ReentrantReadWriteLock.ReadLock readLock=lock.readLock();
   ReentrantReadWriteLock.WriteLock writeLock= lock.writeLock();
- 优点:提高并发性,数据保护
- 缺点:写操作阻塞,可能导致死锁,数据一致性问题

基于锁的占有权是否可重入

可重入锁

当线程已经获取了锁后,它可以再次请求并成功获得同一把锁,从而避免了在递归调用或嵌套同步块中产生的死锁风险

可重入锁主要可以通过以下三种方式实现:

1.synchronized关键字:synchronized关键字实现的隐式锁就是一种可重入锁。
2.ReentrantLock:java.util.concurrent.locks.ReentrantLock类实现了Lock接口,提供了显式的可重入锁功能,它允许更细粒度的控制,例如支持公平锁、非公平锁,以及可中断锁、限时锁等。
3.ReentrantReadWriteLock:ReentrantReadWriteLock 是一种特殊的可重入锁,它通过读写锁的设计,既实现了可重入特性的线程安全,又能高效地处理读多写少的并发场景。

不可重入锁

核心特征在于禁止同一个线程在已经持有锁的前提下再度获取相同的锁。若一个线程已取得不可重入锁,在其执行路径中遇到需要再次获取该锁的场景时,该线程将会被迫等待,直至原先获取的锁被释放,其他线程才有可能获取并执行相关临界区代码。

基于锁的获取公平性

公平锁

- 公平锁是一种线程调度策略,在多线程环境下,当多个线程尝试获取锁时,锁的分配遵循“先请求先服务”(First-Come, First-Served, FCFS)原则,即按照线程请求锁的顺序来分配锁资源。
- 公平锁的实现,可以通过java.util.concurrent.locks.ReentrantLock的构造函数传入true参数,可以创建一个公平的ReentrantLock实例。
    ReentrantLock fairLock = new ReentrantLock(true); //创建一个公平锁

非公平锁

非公平锁是一种线程调度策略,在多线程环境下,当多个线程尝试获取锁时,锁的分配不遵循“先请求先服务”(First-Come, First-Served, FCFS)原则,而是允许任何等待锁的线程在锁被释放时尝试获取,即使其他线程已经在等待队列中等待更长时间。非公平锁在某些场景下可以提高系统的并发性能,因为它允许刚释放锁的线程或者其他新到达的线程立刻获取锁,而不是强制排队等待。

基于对共享资源的访问方式

悲观锁

- 悲观锁(Pessimistic Lock)是一种并发控制策略,它假设在并发环境下,多个线程对共享资源的访问极有可能发生冲突,因此在访问资源之前,先尝试获取并锁定资源,直到该线程完成对资源的访问并释放锁,其他线程才能继续访问。
- 悲观锁的主要作用是在多线程环境中防止数据被并发修改,确保数据的一致性和完整性
- 当一个线程获取了悲观锁后,其他线程必须等到锁释放后才能访问相应资源,从而避免了数据竞态条件和脏读等问题。
- 悲观锁适合写操作较多且读操作较少的并发场景。
- 频繁的加锁和解锁操作可能带来较大的性能消耗,尤其是在高并发场景下,可能导致线程频繁上下文切换。可能导致死锁,在读多写少的场景下,悲观锁可能导致大量的读取操作等待,降低系统的并发能力和响应速度。

乐观锁

- 一种并发控制策略,它基于乐观假设:即在并发访问环境下,认为数据竞争不太可能发生,所以在读取数据时并不会立即加锁。乐观锁适用于读多写少的场景或者并发较少的场景。
- 乐观锁通过CAS(Compare and Swap / Compare and Set)算法实现,而数据库层面我们常使用版本号或者时间戳等进行控制。
- Java提供了java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicLong等,它们通过CAS操作来实现乐观锁。CAS操作是一个原子指令,它只会修改数据,当且仅当该数据的当前值等于预期值时才进行修改。
- 优点:更高的并发性能,降低死锁可能性
- 缺点:冲突处理成本,循环依赖问题(ABA)

基于锁的升级以及优化

偏向锁

- 偏向锁是Java内存模型中锁的三种状态之一,位于轻量级锁和重量级锁之前。
- 适用于大多数时间只有一个线程访问同步代码块的场景。
- 当一个线程访问同步代码块时,JVM会把锁偏向于这个线程,后续该线程在进入和退出同步代码块时,无需再做任何同步操作,从而大大降低了获取锁和释放锁的开销。

轻量级锁

- 轻量级锁的作用主要是减少线程上下文切换的开销,通过自旋(spin-wait)的方式让线程在一段时间内等待锁的释放,而不是立即挂起线程,这样在锁竞争不是很激烈的情况下,能够快速获得锁,提高程序的响应速度和并发性能。
- 轻量级锁主要作为JVM锁状态的一种,它介于偏向锁和重量级锁之间。当JVM发现偏向锁不再适用(即锁的竞争不再局限于单个线程)时,会将锁升级为轻量级锁。

重量级锁

- 重量级锁是指在多线程编程中,为了保护共享资源而采取的一种较为传统的互斥同步机制,通常涉及到操作系统的互斥量(Mutex)或者监视器锁(Monitor)
- 通过synchronized关键字实现的锁机制在默认情况下就是重量级锁
- 重量级锁适用于:
    1.高并发且锁竞争激烈的场景,因为在这种情况下,保证数据的正确性远比微小的性能损失重要。
    2.对于需要长时间持有锁的操作,因为短暂的上下文切换成本相对于长时间的操作来说是可以接受的。
    3.当同步代码块中涉及到IO操作、数据库访问等耗时较长的任务时,重量级锁能够较好地防止其它线程饿死。

偏向锁、轻量级锁和重量级锁之间的转换条件

1.偏向锁到轻量级锁的转换:
   当有第二个线程尝试获取已经被偏向的锁时,偏向锁就会失效并升级为轻量级锁。这是因为偏向锁假定的是只有一个线程反复获取锁,如果有新的线程参与竞争,就需要进行锁的升级以保证线程间的互斥。
2.轻量级锁到重量级锁的转换:
  当轻量级锁尝试获取失败(CAS操作失败),即出现了锁竞争时,JVM会认为当前锁的持有者无法很快释放锁,因此为了避免后续线程无休止地自旋等待,会将轻量级锁升级为重量级锁。这个转换过程通常发生在自旋尝试获取锁达到一定次数(自旋次数是可配置的)或者系统处于高负载状态时。
3.偏向锁到重量级锁的转换:
  如果当前线程不是偏向锁指向的线程,那么首先会撤销偏向锁(解除偏向状态),然后升级为轻量级锁,之后再根据轻量级锁的规则判断是否需要进一步升级为重量级锁。

分段锁

- 分段锁(Segmented Lock 或 Partitions Lock)是一种将数据或资源划分为多个段(segments),并对每个段分配单独锁的锁机制。
- Java中,分段锁在实现上可以基于哈希表的分段锁,例如Java中的ConcurrentHashMap,将整个哈希表分割为多个段(Segment),每个段有自己的锁,这样多个线程可以同时对不同段进行操作。
- 分段锁适用于大数据结构的并发访问,如高并发环境下对哈希表的操作。以及分布式系统中,某些分布式缓存或数据库系统也采用类似的分片锁策略来提高并发性能。

自旋锁

- 自旋锁(Spin Lock)是一种简单的锁机制
- 自旋锁在持有锁的线程很快释放锁的情况下,可以减少线程的上下文切换开销。
- 工作原理是当一个线程试图获取已经被另一个线程持有的锁时,该线程不会立即进入睡眠状态(阻塞),而是不断地循环检查锁是否已经被释放,直到获取到锁为止。这种“循环等待”的行为被称为“自旋”。