我们都知道在 Java 中为了保证一些操作的安全性,就会涉及到使用锁,但是你对 Java 的锁了解的有多少呢?Java 都有哪些锁?以及他们是怎么实现的,今天了不起就来说说关于 Java 的锁。
乐观锁(Optimistic Locking)是一种在数据读取时不会阻塞其他读取或写入操作的锁策略,但在更新时会检查在此期间是否有其他操作修改了数据。如果数据已被修改,则更新操作会失败,通常是通过重试或抛出异常来处理。
在 Java 中,乐观锁通常是通过版本号、时间戳或其他状态信息来实现的。以下是乐观锁在 Java 中的一些常见实现方式:
版本号机制:
时间戳机制:
CAS (Compare-and-Swap) 操作:
JPA 和 Hibernate 的乐观锁:
悲观锁(Pessimistic Locking)是一种在数据处理过程中,总是假设最坏的情况来避免数据并发问题的锁策略。在Java中,悲观锁通常在数据被访问时就立即加锁,以保证在此期间其他任何事务都不能修改这个数据,直到该事务完成为止。
Java中实现悲观锁的常见方式有以下几种:
数据库行级锁和表级锁:
Java中的synchronized关键字:
ReentrantLock类:
读写锁(ReadWriteLock):
分布式锁:
在使用悲观锁时,需要注意死锁和性能问题。死锁是指两个或多个线程无限期地等待对方释放资源的情况。性能问题则可能由于锁的粒度过大(如表级锁)导致并发性能下降。
悲观锁:假设最坏的情况,每次访问数据时都会锁定数据,防止其他事务修改。
乐观锁:假设最好的情况,允许其他事务并发访问数据,但在更新时会检查数据是否被修改。
选择哪种锁策略取决于应用的具体需求和并发场景。使用乐观锁时,需要注意处理更新失败的情况,通常是通过重试、抛出异常或给用户反馈来实现的。
Java中的递归锁(ReentrantLock)是java.util.concurrent.locks包下提供的一种可重入的互斥锁,它是悲观锁的一种实现。递归锁允许一个线程多次获取同一个锁,而不会造成死锁,这对于某些需要递归调用或者在一个线程中多次需要获取同一个锁的场景非常有用。
递归锁的几个特性:
可重入性:如果一个线程已经拥有了一个递归锁,那么它可以再次获取该锁而不会阻塞。每次获取锁,都会增加锁的持有计数;每次释放锁,都会减少持有计数。只有当持有计数减少到0时,其他线程才能获取该锁。
公平性:递归锁可以是公平的也可以是非公平的。公平性意味着锁的获取是按照线程请求锁的顺序来的,而非公平性则不保证顺序。公平的递归锁可以减少“线程饥饿”的问题,但可能会降低性能。
既然我们说她是一个悲观锁的实现,那么是不是可以和 synchronized 比较一下,有什么不同呢?
与Java内置的synchronized关键字相比,递归锁提供了更高的灵活性和更好的性能控制。例如,递归锁支持尝试获取锁(tryLock()方法)、定时获取锁(tryLock(long timeout, TimeUnit unit)方法)以及中断等待锁的线程(lockInterruptibly()方法)。
我们看一下递归锁的示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class RecursiveLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 临界区代码
// ...
someNestedMethod();
// ...
} finally {
lock.unlock();
}
}
private void someNestedMethod() {
lock.lock();
try {
// 嵌套调用中需要同步的代码
// ...
} finally {
lock.unlock();
}
}
}
在上面的示例中,someMethod方法调用了someNestedMethod方法,并且两者都需要获取同一个递归锁。由于ReentrantLock是可重入的,所以这种调用不会造成死锁。
Java中的读写锁(ReadWriteLock)是一种允许多个读线程和单个写线程访问共享资源的同步机制。ReadWriteLock接口在java.util.concurrent.locks包中定义,它包含两个锁:一个读锁和一个写锁。
读写锁的特性:
读共享:在没有线程持有写锁时,多个线程可以同时持有读锁来读取共享资源。这可以提高并发性能,因为读操作通常不会修改数据,所以允许多个读线程并发访问是安全的。
写独占:当一个线程持有写锁时,其他线程既不能获取读锁也不能获取写锁。这是为了确保写操作对共享资源的独占访问,从而防止数据不一致。
Java中ReadWriteLock接口的主要实现类是ReentrantReadWriteLock,它提供了可重入的读写锁实现。ReentrantReadWriteLock有两个重要的方法:readLock()和writeLock(),分别用于获取读锁和写锁。
我们看看示例代码:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int data;
public void readData() {
lock.readLock().lock(); // 获取读锁
try {
// 读取共享资源
System.out.println("Reading data: " + data);
} finally {
lock.readLock().unlock(); // 释放读锁
}
}
public void writeData(int newData) {
lock.writeLock().lock(); // 获取写锁
try {
// 修改共享资源
this.data = newData;
System.out.println("Writing data: " + data);
} finally {
lock.writeLock().unlock(); // 释放写锁
}
}
}
在这个例子中,readData方法使用读锁来读取data字段,而writeData方法使用写锁来修改data字段。当多个线程调用readData时,它们可以同时读取数据而不会相互阻塞,除非有一个线程正在调用writeData并持有写锁。
需要注意的是,ReentrantReadWriteLock还有一个构造方法,它接受一个布尔值参数fair,用于指定锁是否应该是公平的。如果设置为true,则等待时间最长的线程将优先获得锁。但是,公平锁可能会降低性能,因为需要维护一个有序的等待队列。