Java的独占锁和共享锁

Java
298
0
0
2024-05-28

昨天了不起带着大家一起学习了关于这个乐观锁,悲观锁,递归锁以及读写锁,今天我们再来看看这个关于 Java 的其他的锁,大家都了解 Java 的锁有很多种,我们今天再来介绍四种锁。

公平锁

Java 中的公平锁是一种多线程同步机制,它试图按照线程请求锁的顺序来分配锁。公平锁的主要目标是避免“线程饥饿”问题,即某些线程长时间得不到执行的情况。

在 Java 的 java.util.concurrent.locks 包中,ReentrantLock 是一个可重入的互斥锁,它提供了公平锁和非公平锁两种策略。当你创建一个 ReentrantLock 实例时,可以指定它是否为公平锁:

// 创建一个公平锁  
ReentrantLock fairLock = new ReentrantLock(true);  

在公平锁策略中,等待时间最长的线程将获得锁。公平锁通过维护一个队列来跟踪等待锁的线程,并按照它们进入队列的顺序为它们分配锁。然而,需要注意的是,公平锁并不能完全保证公平性,因为线程调度仍然受到操作系统和 JVM 的影响。

公平锁的一个主要缺点是性能。由于需要维护一个队列来跟踪等待锁的线程,并且在线程释放锁时需要唤醒等待队列中的下一个线程,因此公平锁通常比非公平锁具有更高的开销。此外,在高并发场景下,公平锁可能会导致更高的上下文切换率,从而降低系统性能。

非公平锁

其实我们在看到了上面的公平锁之后,那么就很容易的去了解这个非公平锁,因为非公平锁是与公平锁相对的一种多线程同步机制。在非公平锁策略中,锁的分配并不保证按照线程请求锁的顺序来进行。这意味着,即使有一个线程已经等待了很长时间,新到来的线程仍然有可能立即获得锁。

非公平锁通常具有更高的吞吐量,因为它们减少了维护等待队列所需的开销。当线程尝试获取锁时,它不必检查或加入等待队列,而是直接尝试获取锁。如果锁当前可用,线程就可以立即获得锁并执行,而不需要等待其他线程。

在 Java 的 java.util.concurrent.locks 包中,ReentrantLock 类的默认构造函数创建的就是一个非公平锁:

// 创建一个非公平锁  
ReentrantLock unfairLock = new ReentrantLock();

非公平锁的优势在于它们通常能够更有效地利用系统资源,特别是在高并发场景下。由于减少了线程间的切换和等待,非公平锁通常能够提供更高的性能。

然而,非公平锁的一个潜在缺点是它们可能会导致线程饥饿。如果有一个或多个线程持续地被新到来的线程抢占,那么这些等待的线程可能会长时间得不到执行。这种情况在高负载或资源竞争激烈的系统中尤其可能发生。

在选择使用公平锁还是非公平锁时,应该根据应用程序的具体需求进行权衡。如果系统对公平性有严格要求,或者想要避免线程饥饿问题,那么公平锁可能是一个更好的选择。如果系统更关注性能,并且可以接受一定程度的不公平性,那么非公平锁可能更加合适。

共享锁

在Java中,共享锁(Shared Lock)是一种允许多个线程同时读取资源,但在写入资源时只允许一个线程独占的锁。这种锁通常用于提高读取操作的并发性,因为读取操作通常不会修改数据,所以允许多个线程同时进行读取是安全的。

Java的java.util.concurrent.locks包中的ReentrantReadWriteLock类就是一种实现了共享锁和独占锁(排他锁)机制的读写锁。在这个锁中,读锁是共享的,写锁是独占的。

我们来看看示例代码:

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();  
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();  
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();  
  
// 读取数据时获取读锁  
readLock.lock();  
try {  
    // 读取共享资源  
} finally {  
    readLock.unlock();  
}  
  
// 修改数据时获取写锁  
writeLock.lock();  
try {  
    // 修改共享资源  
} finally {  
    writeLock.unlock();  
}

在上面的代码中,多个线程可以同时获取读锁来读取数据,但当一个线程获取了写锁时,其他线程既不能获取读锁也不能获取写锁,直到写锁被释放。

ReentrantReadWriteLock有两种模式:公平模式和非公平模式。在公平模式下,等待时间最长的线程将优先获得锁;而在非公平模式下,锁的分配不保证任何特定的顺序,新到来的线程可能立即获得锁。

要注意的是,尽管读锁是共享的,但写锁是独占的,并且写锁具有更高的优先级。这意味着当一个线程持有写锁时,其他线程无法获取读锁或写锁。此外,如果一个线程正在读取数据,并且有其他线程请求写锁,那么写线程将会被阻塞,直到所有读线程释放读锁。

ReentrantReadWriteLock的读锁和写锁都是可重入的,这意味着一个线程可以多次获取同一个锁而不会导致死锁。

使用共享锁可以显著提高读取密集型应用的性能,因为它允许多个读取线程并发执行,而写入密集型应用可能会因为写锁的竞争而受到限制。

独占锁

在Java中,独占锁(Exclusive Lock)是一种同步机制,它确保在给定时间内只有一个线程能够访问特定的资源或代码块。当一个线程持有独占锁时,其他试图获取同一锁的线程将会被阻塞,直到持有锁的线程释放该锁。

java.util.concurrent.locks包中的ReentrantLock就是一种独占锁(也被称为排他锁或互斥锁)的实现。此外,synchronized关键字在Java中也被用作实现独占锁的一种方式。

我们看看独占锁的示例代码:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ExclusiveLockExample {  
    private final ReentrantLock lock = new ReentrantLock();  
    private int sharedData;  
  
    public void updateData(int newValue) {  
        lock.lock(); // 获取独占锁  
        try {  
            // 在此区域内只有一个线程能够执行  
            sharedData = newValue;  
        } finally {  
            lock.unlock(); // 释放独占锁  
        }  
    }  
  
    public int readData() {  
        lock.lock(); // 获取独占锁以进行读取(虽然通常读取操作可以使用读锁来允许多个线程并发读取)  
        try {  
            // 在此区域内只有一个线程能够执行  
            return sharedData;  
        } finally {  
            lock.unlock(); // 释放独占锁  
        }  
    }  
}

在这个例子中,updateData和readData方法都使用了独占锁来确保同时只有一个线程能够访问sharedData变量。

上面这个示例是使用的ReentrantLock的独占锁,既然我们说了 synchronized 关键字也是可以的,我们看看使用这个 synchronized 关键字的独占锁:

public class SynchronizedExample {  
    private int sharedData;  
  
    public synchronized void updateData(int newValue) {  
        // 在此区域内只有一个线程能够执行  
        sharedData = newValue;  
    }  
  
    public synchronized int readData() {  
        // 在此区域内只有一个线程能够执行  
        return sharedData;  
    }  
}

在synchronized这个例子中,updateData和readData方法都被声明为synchronized,这意味着它们在同一时间内只能由一个线程访问。synchronized关键字提供了一种简便的方式来实现独占锁,而不需要显式地创建锁对象。

独占锁对于保护临界区(critical sections)非常有用,临界区是一段代码,它访问或修改共享资源,并且必须被串行执行以防止数据不一致。然而,独占锁可能会降低并发性,因为它阻止了多个线程同时访问被保护的资源。因此,在设计并发系统时,需要仔细权衡独占锁的使用。

所以关于这四种锁,你了解了么?