Java lock 关键字的原理用法详解
原理
在 Java 中,lock
是一个接口 java.util.concurrent.locks.Lock
的实现。与 synchronized 关键字相比,lock 提供了更加灵活的同步机制。
lock 接口提供了两个主要方法:lock()
和 unlock()
。当一个线程调用 lock()
方法时,它会尝试获取锁,如果锁已经被其他线程持有,则该线程将被阻塞,直到获取到锁为止。而当一个线程调用 unlock()
方法时,它会释放锁,以便其他线程可以获取锁并执行。
lock 的原理是通过显示地获取和释放锁来实现线程之间的同步。lock 可以实现更细粒度的锁定,允许线程按照自己的需要来获取和释放锁,从而提高程序的并发性能。
分类
lock属于显式锁(Explicit Lock),即使用 java.util.concurrent.locks.Lock
接口及其实现类提供的锁机制。显式锁需要手动获取和释放,提供了更灵活的控制能力。常见的实现类有 ReentrantLock
、ReadWriteLock
等。
ReentrantLock
和 ReadWriteLock
都是 Java 中用于实现线程同步的锁机制,但它们在功能和应用场景上有所不同。
ReentrantLock
ReentrantLock
是一个可重入锁,也是 Lock
接口的实现类。它提供了与 synchronized
关键字类似的线程同步功能,但具有更多的灵活性和高级特性。
ReentrantLock
具有以下特点:
- 可重入性:同一个线程可以多次获取同一个
ReentrantLock
而不会被阻塞,这样可以避免死锁的发生。 - 公平性选择:可以选择公平锁(Fair Lock)或非公平锁(Nonfair Lock)。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许插队,可能导致某些线程长时间等待。
- 锁的中断支持:
ReentrantLock
提供了对锁的中断支持,即当一个线程等待锁时,可以通过调用lockInterruptibly()
方法使其可被中断。 - 条件变量:
ReentrantLock
提供了Condition
接口及其实现类ConditionObject
,用于实现更复杂的线程通信和等待/唤醒机制。
使用示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 执行需要同步的代码块
} finally {
lock.unlock();
}
}
}
ReadWriteLock
ReadWriteLock
是一个读写锁,它允许多个线程同时读取共享资源,但在写操作时只能有一个线程进行,以保证数据一致性和并发性能。
ReadWriteLock
包含两个关键接口:
ReadLock
:用于获取读锁。多个线程可以同时持有读锁,只要没有线程持有写锁。WriteLock
:用于获取写锁。只有当没有线程持有读锁或写锁时,才能成功获取写锁。
ReadWriteLock
具有以下特点:
- 读写分离:允许多个线程同时读取共享资源,提高并发性能。
- 写操作互斥:在写操作时,只能有一个线程持有写锁,以确保数据一致性。
- 降级支持:允许从写锁降级为读锁,即在获取写锁之后再获取读锁,以避免阻塞其他线程的读操作。
使用示例:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Example {
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void readData() {
lock.readLock().lock();
try {
// 执行读取共享资源的操作
} finally {
lock.readLock().unlock();
}
}
public void writeData() {
lock.writeLock().lock();
try {
// 执行写入共享资源的操作
} finally {
lock.writeLock().unlock();
}
}
}
注意事项
在使用 lock 关键字时,需要注意以下几点:
- 在使用 lock 锁时,要确保在获取锁之后,一定要释放锁,否则会导致死锁的发生。
- 通常情况下,在 finally 块中释放锁是一个好的实践,以确保无论是否发生异常,都能正确地释放锁。
- lock 提供了更加灵活的同步机制,可以实现更细粒度的锁定,但同时也增加了代码的复杂性,需要仔细处理锁的获取和释放逻辑,避免出现潜在的问题。
- 当使用 lock 锁时,要确保所有访问共享资源的方法都使用同一个 lock 对象进行同步,否则仍然可能出现线程安全问题。
- lock 可以配合条件变量等高级特性一起使用,提供更强大的同步控制能力。
- 在使用
ReentrantLock
和ReadWriteLock
时,需要确保在适当的位置调用unlock()
方法来释放锁,以避免死锁的发生。 - 对于
ReadWriteLock
,要根据实际需求选择合适的读写策略,避免频繁地进行锁的切换
乐观锁悲观锁延伸
乐观锁和悲观锁是并发控制中常见的两种策略。
乐观锁:
- 乐观锁假设在大多数情况下,读操作不会与其他操作发生冲突。
- 在乐观锁中,线程在读取数据时不会加锁,而是在更新数据之前检查是否有其他线程对该数据进行了修改。
- 如果检测到其他线程已经修改了数据,则当前线程可以进行相应的冲突处理,如重试操作或放弃更新。
- 常见的乐观锁实现方式包括使用版本号、时间戳等机制来标识数据的状态和变化。
使用实例:
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
// 读取当前值
oldValue = counter.get();
// 计算新值
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
}
在上述示例中,使用 AtomicInteger
类来实现乐观锁。AtomicInteger
提供了原子性的读取和更新操作,并通过 compareAndSet
方法进行比较并设置新值。如果在比较和设置过程中发现有其他线程修改了值,则重试。
悲观锁:
- 悲观锁假设在并发环境中,读操作和写操作之间存在冲突。
- 在悲观锁中,线程在访问数据之前会先获取锁,确保在任何时候只有一个线程可以访问共享资源。
- 当一个线程获得了悲观锁后,其他线程需要等待锁的释放才能访问共享资源。
- 常见的悲观锁实现方式包括使用 synchronized 关键字、ReentrantLock 类等来实现锁机制。
乐观锁适用于读操作较多、冲突发生的概率较低的场景,它减少了锁的开销,提高了并发性能。悲观锁适用于冲突频繁发生的场景,通过加锁来保证数据的一致性和安全性。
使用实例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock();
try {
// 执行更新操作
counter++;
} finally {
lock.unlock();
}
}
}
在上述示例中,使用 ReentrantLock
实现悲观锁。在 increment
方法中,首先获取锁,在执行更新操作后释放锁。这样可以确保在任何时候只有一个线程能够访问共享资源,保证数据的一致性和安全性。
需要注意的是,在使用悲观锁时,一定要确保在适当的位置释放锁,以避免死锁或资源泄露的情况发生。