目录
1. synchronized关键字
1.1 基本概念
1.2 内置锁
1.3 限制
2. ReentrantLock
2.1 概述
2.2 公平性与非公平性
2.3 条件变量
3. 读写锁(ReadWriteLock)
3.1 概念
3.2 适用场景
4. StampedLock
4.1 概述
4.2 乐观读与悲观读
4.3 适用场景
5. 性能比较与选择
6. 总结
在Java编程中,多线程并发是一个常见的场景。为了保证线程安全性,Java提供了一系列的锁机制,用于控制对共享资源的访问。这些锁机制在并发编程中起着至关重要的作用,确保多个线程能够协同工作而不产生竞态条件或数据不一致的问题。本文将深入探讨Java中的锁机制,包括传统的synchronized关键字、ReentrantLock类以及更为高级的读写锁和StampedLock。
1. synchronized关键字
1.1 基本概念
Java的synchronized
关键字是最基本的锁机制之一。它可以用来修饰方法或代码块,确保同一时刻只有一个线程能够访问被锁定的代码。
public synchronized void synchronizedMethod() {
// 线程安全的操作
}
1.2 内置锁
synchronized
使用的是内置锁,也称为监视器锁。每个Java对象都有一个与之关联的内置锁,通过synchronized
关键字可以对这个锁进行操作。当一个线程试图访问一个被synchronized
修饰的方法或代码块时,它会尝试获取对象的内置锁,如果锁已经被其他线程占用,那么线程将被阻塞,直到获取到锁为止。
1.3 限制
虽然synchronized
是简单易用的锁机制,但它也有一些限制。首先,它是非公平的,不能保证等待时间最长的线程会最先获得锁。其次,一旦线程进入synchronized
代码块,其他线程必须等待,不能中途取消。
2. ReentrantLock
2.1 概述
ReentrantLock
是Java.util.concurrent包中提供的一种更灵活的锁机制。与synchronized
不同,ReentrantLock
允许线程在获得锁之后再次进入同步代码块,即支持重入。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 线程安全的操作
} finally {
lock.unlock();
}
}
}
2.2 公平性与非公平性
ReentrantLock
提供了公平性选择。在构造函数中,可以选择是否使用公平锁。公平锁按照线程请求锁的顺序进行获取,而非公平锁允许插队,可能会导致某些线程一直获取不到锁。
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁
2.3 条件变量
ReentrantLock
还支持条件变量,可以通过newCondition
方法创建。条件变量允许线程在获取锁之后等待或者唤醒,提供了更为灵活的线程通信方式。
import java.util.concurrent.locks.Condition;
public class ReentrantLockWithCondition {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
3. 读写锁(ReadWriteLock)
3.1 概念
ReadWriteLock
接口提供了一种更为精细的锁分离机制,分为读锁和写锁。读锁可以被多个线程同时持有,但写锁是独占的,只能被一个线程持有。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void readMethod() {
readWriteLock.readLock().lock();
try {
// 执行读操作
} finally {
readWriteLock.readLock().unlock();
}
}
public void writeMethod() {
readWriteLock.writeLock().lock();
try {
// 执行写操作
} finally {
readWriteLock.writeLock().unlock();
}
}
}
3.2 适用场景
ReadWriteLock
适用于读多写少的场景,可以提高系统的并发性能。读锁的共享特性使得多个线程可以同时读取共享资源,而写锁的独占特性保证了写操作的原子性。
4. StampedLock
4.1 概述
StampedLock
是Java 8引入的新锁机制,结合了读写锁和乐观锁的特点。它引入了"stamp"的概念,用来标记锁的状态。
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock stampedLock = new StampedLock();
public void readMethod() {
long stamp = stampedLock.tryOptimisticRead();
try {
// 乐观读操作
if (!stampedLock.validate(stamp)) {
// 转为悲观读
stamp = stampedLock.readLock();
try {
// 执行悲观读操作
} finally {
stampedLock.unlockRead(stamp);
}
}
} finally {
stampedLock.unlock(stamp);
}
}
public void writeMethod() {
long stamp = stampedLock.writeLock();
try {
// 执行写操作
} finally {
stampedLock.unlockWrite(stamp);
}
}
}
4.2 乐观读与悲观读
StampedLock
引入了乐观读和悲观读的概念。在乐观读模式下,线程尝试获取一个标记(stamp),然后进行读操作,最后通过validate
方法验证标记是否仍然有效。如果标记无效,表示在读操作期间有写操作发生,需要切换为悲观读模式。悲观读模式下,线程直接获取读锁,执行读操作,然后释放读锁。
4.3 适用场景
StampedLock
适用于读操作远远多于写操作的情况,并且乐观读是常态的场景。相较于ReadWriteLock
,StampedLock
提供了更高的并发性能。
5. 性能比较与选择
在选择锁的时候,需要根据具体的业务场景和性能需求来进行权衡。以下是一些选择锁的一些建议:
-
如果并发要求不高,可以使用
synchronized
关键字,它简单易用,不需要手动释放锁,适用于简单的线程同步场景。 -
如果需要更灵活的控制和可重入特性,可以选择
ReentrantLock
,并且可以根据实际情况选择公平锁或非公平锁。 -
如果读操作远远多于写操作,可以选择
ReadWriteLock
,提高系统的并发性能。 -
如果乐观读是常态,并且读操作频繁,可以考虑使用
StampedLock
,它提供了更高的并发性能。
6. 总结
Java中的锁机制为多线程编程提供了强大的支持,开发人员可以根据实际需求选择合适的锁来保证线程安全性。从简单的synchronized
关键字到更为灵活的ReentrantLock
,再到适用于读多写少场景的ReadWriteLock
,以及引入了乐观读的StampedLock
,Java提供了丰富的锁机制,帮助开发人员更好地处理并发编程中的各种情况。在实际应用中,合理选择锁机制是提高系统性能和稳定性的关键一步。