疯狂敲代码的老刘

疯狂敲代码的老刘

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

引言

在并发编程的世界中,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实现的缓存系统的代码示例:

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来控制对缓存的并发访问。当需要读取缓存时,我们获取读锁,这允许多个线程同时读取缓存;当需要写入缓存时,我们获取写锁,这确保了只有一个线程能够写入数据,从而保证了数据的一致性。

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

写锁的状态减少和释放

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

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

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

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

总结

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

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

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

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

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

11-05 05:25