java基础07-java中的锁
文章目录
一、什么是AQS
AQS(AbstractQueuedSynchronizer)抽象队列同步器,它提供了实现锁和同步器的基础框架。AQS是一个抽象类,它通过使用FIFO队列来管理线程的排队和等待状态。
AQS提供了一些方法供子类实现,以实现具体的锁和同步器。它定义了以下两种同步状态:
- 0表示未被锁定状态。
- 大于0表示被锁定状态。
AQS提供了以下核心方法:
acquire(int arg):尝试获取锁,如果失败则进入等待队列。release(int arg):释放锁,唤醒等待队列中的线程。tryAcquire(int arg):尝试以非阻塞方式获取锁。tryRelease(int arg):尝试以非阻塞方式释放锁。tryAcquireShared(int arg):尝试以非阻塞方式获取共享锁。tryReleaseShared(int arg):尝试以非阻塞方式释放共享锁。
AQS提供了公平锁和非公平锁的支持,并且可以被用来构建各种类型的同步器,例如ReentrantLock、CountDownLatch、Semaphore等。
AQS是实现锁和同步器的基础框架,它简化了锁和同步器的实现,提供了高度的灵活性和可扩展性。它在Java并发编程中扮演着重要的角色,为实现线程安全和并发控制提供了强大的支持。
二、什么是CAS
CAS操作涉及三个操作数:要更新的变量(V)、期望值(E,表示更新前的“旧值”)和新值(N,表示要更新为的新值)。CAS操作的执行过程如下:
- 从内存中读取当前值V。
- 比较当前值V与期望值E:
- 如果相等(说明V未被其他线程修改),将V更新为N。
- 如果不相等(说明V已被其他线程修改),什么也不做。
CAS操作是原子的,意味着在执行过程中不会被其他线程中断或干扰。它解决了传统的锁机制中存在的线程竞争和死锁的问题,提供了一种高效且无锁的并发控制方式。
CAS常用于并发编程中的无锁算法和非阻塞算法的实现,例如Java中的Atomic类和java.util.concurrent.atomic包中的原子操作类就是基于CAS实现的。它在实现线程安全的数据结构和并发控制时具有重要的作用。
三、锁分类
我们通过特性将锁进行分组归类
1、悲观锁和乐观锁
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
在乐观锁的实现中,通常会使用版本号(Version)或时间戳(Timestamp)来标识数据的版本或修改时间。每个线程在读取数据时,会将版本号或时间戳保存下来。当进行数据更新时,线程会检查当前数据的版本或时间戳是否与自己保存的值相匹配。如果匹配成功,说明在读取数据之后没有其他线程修改过,可以进行更新操作;如果匹配失败,说明有其他线程修改了数据。
悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
2、自旋锁和适应性自旋锁
自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态切换所消耗的时间有可能比用户代码执行的时间还要长。
自旋锁是一种基于循环的锁机制,用于解决多线程并发访问共享资源时的同步问题。在自旋锁中,当一个线程请求获取锁资源时,如果发现锁已被其他线程占用,该线程会一直循环检查锁是否被释放,而不是立即进入阻塞状态。这样的循环称为自旋。
自旋锁的核心思想是通过忙等待的方式,避免线程进入阻塞状态,从而减少线程上下文切换的开销。它适用于以下情况:
-
锁竞争激烈但持有锁的时间很短的情况,这样可以避免线程频繁地切换到阻塞状态。
-
线程在等待锁的时间较短,自旋等待比进入阻塞状态更高效。
需要注意的是,自旋锁适用于多核CPU或多线程环境,因为在单核CPU上使用自旋锁可能会导致其他线程无法执行,造成资源浪费。
通过自旋等待释放锁] C --> D{再次尝试获取锁} D -->|成功| E[成功获取锁] D -->|失败| C B -->|非自旋锁| F[CPU切换状态
使当前线程休眠] F --> G[CPU切换线程
执行其它操作] G --> H[占用锁的线程释放了锁] H --> I[恢复现场] I --> D
适应性自旋锁
适应性自旋锁是一种自旋锁的优化技术,它根据当前线程在过去的自旋次数和自旋时间的表现来动态调整自旋的策略,以提高性能。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
3、公平锁和非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得的原则。当一个线程释放锁时,等待时间最长的线程将获得锁的使用权。公平锁可以确保锁的获取是按照公平的方式进行,避免了某些线程长期等待的情况,但是可能会引起线程上下文切换的频繁发生,从而降低系统的整体性能。
非公平锁
非公平锁是指多个线程竞争锁时,不考虑申请锁的顺序,直接去竞争锁的使用权。在获取锁的过程中,先尝试直接获取锁,如果失败了再进入等待队列等待。非公平锁不会先查看等待队列中是否有等待的线程,而是直接尝试获取锁,这样可以减少线程上下文切换的次数,在一定程度上提高系统的吞吐量,但是可能会导致某些线程长期等待的情况,不公平的现象。
需要注意的是,非公平锁并不意味着完全不考虑线程的顺序,只是在竞争锁的时候不严格按照申请锁的顺序进行。在某些情况下,非公平锁获取锁时获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
4、可重入锁和不可重入锁
可重入锁
可重入锁是一种支持线程重复获取自身已经持有的锁的机制,也称为递归锁。当一个线程获取到锁后,可以再次获取该锁而不会被阻塞,这种机制可以避免死锁的发生。
可重入锁通过为每个锁关联一个持有计数器来实现。线程第一次获取锁时,计数器加一;每次成功获取锁时,计数器加一;释放锁时,计数器减一。只有当计数器归零时,锁才算完全释放,其他线程才能获取该锁。
|
|
不可重入锁
顾名思义与可重入锁相反。
四、java中常用的锁
1、synchronized
synchronized是一种非自旋、可重入锁的悲观锁。默认情况下非公平锁,可以可以通过在 synchronized 关键字之前加上 synchronized 的形式来使用公平锁。
|
|
2、ReentrantLock
ReentrantLock是一种可重入的悲观锁、可根据需求选择是否是公平锁。在非公平锁模式下是自旋锁。
|
|
3、ReadWriteLock
读写锁 ReadWriteLock(接口) | ReentrantReadWriteLock(实现类)。
|
|
ReadWriteLock维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
ReadWriteLock是一种可重入的悲观锁,默认情况下非公平锁。
公平选择性:
-
非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
-
公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
4、StampedLock
StampedLock是一种乐观锁,不是自旋锁,也不是公平锁,同时也不是可重入锁。
文章作者 necor 上次更新 2024-02-21