面试10000次依然会问的【ReentrantLock】,你还不会?

引言

ReentrantLock是一个实现了重入特性的互斥锁,提供了比synchronized关键字更加灵活的锁定机制。ReentrantLock属于java.util.concurrent.locks包,是Java并发API的一部分。

与传统的synchronized方法或代码块相比,ReentrantLock提供了更丰富的功能,如可中断的锁获取操作、尝试非阻塞地获取锁、公平锁以及支持多个条件变量等。

在多线程环境下,ReentrantLock能够确保线程在访问共享资源时的互斥性,从而避免了资源的竞争和潜在的数据不一致性问题。通过其提供的锁定机制,开发者可以构建出更加健壮和高效的并发应用程序。

特别是在读多写少的场景中,ReentrantLock的读写锁(ReentrantReadWriteLock)能够显著提高程序的性能,因为它允许多个线程同时对资源进行读取,而写入则需要独占访问。

ReentrantLock不仅加强了程序的并发性能,也为复杂的同步策略提供了可能,是并发编程中不可或缺的工具之一。

ReentrantLock与WriteLock的区别

ReentrantLock和WriteLock都是Java并发编程中用于控制多线程访问共享资源的锁机制。ReentrantLock是一个完全独立的锁,提供了比synchronized关键字更灵活的锁定操作,它支持公平锁和非公平锁,能够响应中断,还能够尝试非阻塞地获取锁。

而WriteLock是ReentrantReadWriteLock的一部分,它专门用于写操作,确保了写操作的原子性和可见性。在获取WriteLock时,必须确保没有其他线程正在读或写,这意味着WriteLock在获取锁的过程中需要同时考虑读锁和写锁的状态,而ReentrantLock则只需要考虑自身的状态。

ReentrantLock支持锁的重入,即同一个线程可以多次获取同一把锁,而WriteLock作为读写锁的一部分,也支持锁的重入。

ReentrantLock与Semaphore的区别

ReentrantLock和Semaphore虽然都是并发编程中的同步工具,但它们的用途和工作方式有所不同。ReentrantLock是一种独占锁,它可以由同一个线程多次获取,用于实现临界区的互斥访问。ReentrantLock的独占性意味着在锁被释放之前,其他所有请求这个锁的线程都会被阻塞。

相比之下,Semaphore是一个计数信号量,它不是为了互斥访问而设计的,而是用来限制同时访问某一组资源的线程数量。Semaphore可以配置为公平或非公平,而ReentrantLock也提供了这样的配置选项。Semaphore通常用于控制资源池,例如限制最大的数据库连接数。Semaphore允许多个线程同时访问资源,但是一旦达到最大许可数,其他线程则需要等待,直到一个正在访问资源的线程释放了许可。

在使用场景上,ReentrantLock更适合于对象级别的互斥,而Semaphore适用于控制对应用程序范围内资源的访问。

锁的获取与释放

ReentrantLock作为一个独立的独占锁,其获取与释放锁的机制是通过一个叫做抽象队列同步器(AQS)的框架来实现的。当一个线程尝试获取锁时,它会调用AQS的独占获取方法,这个过程可以通过sync.acquire(1)来实现。如果锁未被占用,这个线程将成功获取锁并持有。如果锁已被其他线程持有,尝试获取锁的线程将会被加入到一个等待队列中,并在锁被释放时按照一定的策略(如公平或非公平)被唤醒。

释放锁的过程则是通过调用tryRelease(int releases)方法来实现的,这个方法会在同步状态减至零时完成。在ReentrantLock中,每个获取锁的操作都会使同步状态增加,而每个释放锁的操作都会使其减少。当同步状态回到零时,表示锁已经完全释放,等待队列中的其他线程可以尝试获取锁。

重入性的实现原理

ReentrantLock的重入性是指线程可以重复获取它已经持有的锁。这一特性是通过AQS中的同步状态来实现的。当线程第一次获取锁时,AQS会记录下锁的持有者,并将同步状态设置为1。如果当前线程再次尝试获取这个锁,它会检查自己是否为当前的持有者。如果是,它将直接增加同步状态而不是进入等待队列。

在ReentrantLock的实现中,同步状态的增加和减少代表了锁的获取和释放次数。只有当同步状态减至零时,锁才被认为是完全释放的,这时其他线程才有机会获取锁。这种设计允许了同一个线程在没有完全释放锁的情况下,多次进入由这个锁保护的代码区域,从而实现了锁的重入性。

通过这种方式,ReentrantLock确保了在多线程环境下,同一个线程可以安全地重复进入锁定的代码区,而不会导致死锁。同时,这也意味着线程在每次进入时都必须记得释放锁,否则其他线程将永远无法获取到锁,从而导致系统的不稳定。

读锁和写锁的实现机制

ReentrantReadWriteLock提供了两种锁:读锁(ReadLock)和写锁(WriteLock)。这两种锁的实现机制是为了解决读多写少的并发问题,提高系统性能。

读锁是共享的,允许多个线程同时访问共享资源,但在写线程访问时,所有读线程和其他写线程都会被阻塞。读锁的获取和释放是通过AQS(AbstractQueuedSynchronizer)框架中的同步状态来实现的。当一个线程尝试获取读锁时,如果没有线程持有写锁(即写状态为0),则通过CAS(Compare-And-Swap)操作增加同步状态中的读状态,表示读锁的获取。释放读锁时,同步状态中的读状态相应减少。

写锁是独占的,一次只允许一个线程进行写入操作。当一个线程尝试获取写锁时,它需要检查是否存在其他写锁或读锁。如果没有其他线程持有读锁或写锁,该线程通过AQS独占模式尝试获取锁。获取写锁的过程中,如果有线程持有读锁或其他写锁,当前线程将无法获取写锁,必须等待。

在实现缓存系统时,使用ReentrantReadWriteLock可以提高缓存的读取效率,同时保证写入操作的安全性。例如,当缓存失效时,需要获取写锁来更新缓存,更新后再降级为读锁以允许其他线程读取新缓存。

锁降级的操作和原理

锁降级是指在持有写锁的情况下,先获取读锁,然后释放写锁的过程。这样做可以保持数据的可见性,即使在锁被降级后,其他线程也无法写入数据,因为读锁仍然被持有。Java中的ReentrantReadWriteLock支持锁降级,但不支持锁升级(即在持有读锁的情况下直接获取写锁)。

锁降级的主要用途是在需要保持数据读取的一致性,同时减少锁竞争的场景下。例如,在一个缓存系统中,大部分操作是读取数据,只有在数据失效时才需要写入。使用读写锁可以在不牺牲数据一致性的前提下,提高系统的并发读取性能。

在锁降级的操作中,首先获取写锁以确保对共享数据的独占访问。在修改数据后,我们在释放写锁之前获取读锁,这样即使写锁被释放,其他线程也无法获取写锁来修改数据,但可以获取读锁来读取数据。这就完成了锁降级的过程。最后,在使用完数据后释放读锁。

公平性与性能

ReentrantLock提供了两种锁的获取策略:公平锁和非公平锁。公平锁意味着锁的分配将按照线程请求的顺序来进行,确保了等待时间最长的线程最先获得锁,从而避免了饥饿现象。然而,公平锁可能会导致较多的性能开销,因为维护一个有序队列并在每次锁释放时进行线程调度,会增加额外的开销。

相比之下,非公平锁则不保证请求锁的顺序,允许插队,这通常会导致更高的吞吐量。因为非公平锁减少了线程之间的切换,从而减少了上下文切换的成本。但是,这种策略可能会导致新的线程饥饿,尤其是在高负载时。在实际应用中,非公平锁通常是默认的选择,因为它们在大多数情况下提供了更好的性能。

锁的状态管理

ReentrantLock通过内部类Sync(继承自AbstractQueuedSynchronizer,简称AQS)来管理锁的状态。AQS使用一个int类型的状态变量来表示锁的状态,对于ReentrantLock而言,状态的值表示锁的持有次数。当线程请求锁时,AQS会尝试通过CAS(Compare-And-Swap)操作来改变这个状态值,如果成功,则表示线程获取了锁。

当锁被释放时,状态值相应地减少。当状态值降到0时,表示锁完全释放。由于ReentrantLock是可重入的,同一个线程可以多次获得锁,每次获取锁都会使状态值增加,每次释放锁都会使状态值减少。AQS提供了一种机制来保证状态的安全更新,同时也提供了队列机制来管理那些未能成功获取锁的线程。

通过这种方式,ReentrantLock确保了锁状态的准确性和线程安全性,同时也支持了锁的高级特性,如条件变量(Condition),它们允许线程在某些条件下挂起和唤醒。

实现一个简单的ReentrantReadWriteLock缓存系统

ReentrantReadWriteLock是一种读写锁,它允许多个线程同时读取数据,但是在写入数据时,只允许一个线程进行操作。这种锁机制非常适合实现缓存系统,因为缓存系统通常面临大量的读操作和少量的写操作。下面是一个简单的使用ReentrantReadWriteLock实现的缓存系统的代码示例:

java 复制代码
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.HashMap;
import java.util.Map;

public class CacheWithReadWriteLock {
    private final Map<String, Object> cacheMap = new HashMap<>();
    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
    // 获取缓存中的值
    public Object get(String key) {
        readWriteLock.readLock().lock(); // 获取读锁
        try {
            return cacheMap.get(key);
        } finally {
            readWriteLock.readLock().unlock(); // 释放读锁
        }
    }
    
    // 放入缓存中的值
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock(); // 获取写锁
        try {
            cacheMap.put(key, value);
        } finally {
            readWriteLock.writeLock().unlock(); // 释放写锁
        }
    }
    
    // 其他缓存操作...
}

在这个示例中,我们定义了一个CacheWithReadWriteLock类,它内部使用了一个HashMap来存储缓存数据,以及一个ReentrantReadWriteLock来控制对缓存的并发访问。当需要读取缓存时,我们获取读锁,这允许多个线程同时读取缓存;当需要写入缓存时,我们获取写锁,这确保了只有一个线程能够写入数据,从而保证了数据的一致性。

写锁的状态减少和释放

写锁是一种独占锁,当线程完成写操作后,它需要释放锁,以便其他线程可以访问数据。在ReentrantReadWriteLock中,写锁的释放通常涉及到状态的减少。这是因为ReentrantReadWriteLock支持锁的重入,即同一个线程可以多次获取同一个锁,每次获取锁时都会增加状态计数,每次释放锁时都会减少状态计数。

以下是一个简化的写锁释放过程的代码示例:

java 复制代码
public class CacheWithReadWriteLock {
    // ...(其他代码)

    // 更新缓存并释放写锁
    public void updateCache(String key, Object value) {
        readWriteLock.writeLock().lock(); // 获取写锁
        try {
            // 更新缓存数据
            cacheMap.put(key, value);
        } finally {
            // 在释放写锁前获取读锁,实现锁降级
            readWriteLock.readLock().lock();
            readWriteLock.writeLock().unlock(); // 释放写锁,此时读锁仍然被持有
            // 确保数据可见性,允许其他线程读取更新后的数据
            try {
                // 可以进行一些只需要读锁的操作
            } finally {
                readWriteLock.readLock().unlock(); // 最终释放读锁
            }
        }
    }
}

在这个示例中,updateCache方法首先获取写锁来更新缓存。在更新操作完成后,它在释放写锁之前获取了读锁,这是一种锁降级的操作,它允许线程在保持数据可见性的同时,减少锁的竞争。最后,线程释放了读锁,使得其他线程可以安全地读取更新后的数据。

总结

ReentrantLock 是 Java 并发编程中的一个高级同步机制,它提供了比传统 synchronized 方法和语句更丰富的操作。在现代多线程编程中,ReentrantLock 的关键特性使其成为管理复杂同步需求的强大工具。

ReentrantLock 支持重入性,即线程可以重复获取已经持有的锁,这对于递归调用或者其他需要多次加锁的场景非常有用。其次,ReentrantLock 提供了公平锁和非公平锁的选择,公平锁可以按照线程请求锁的顺序来分配锁,而非公平锁则可能允许后请求的线程先获得锁,这在某些情况下可以减少线程切换,提高效率。

ReentrantLock 还提供了条件变量(Condition),这允许线程在某些条件不满足时挂起,等待特定条件的发生再继续执行,这比 Object 的 wait/notify 机制提供了更细粒度的控制。

在性能方面,ReentrantLock 提供的锁机制通常比 synchronized 更加灵活和高效,尤其是在高竞争环境下。它允许开发者通过精细的锁管理策略来优化并发性能,比如限制锁的范围、分离读写操作等。

ReentrantLock 的这些特性使其在多线程编程中非常有用,尤其是在需要高度并发控制和灵活性的应用程序中。通过合理使用 ReentrantLock,开发者可以构建出既安全又高效的并发应用。

相关推荐
Asthenia041228 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫