🌟 可重入锁 (Reentrant Lock) in Java
📌 定义
- 可重入锁即是指一个线程可以多次获取同一把锁。在Java中,
synchronized
提供的锁就是可重入锁。
📌 为什么需要可重入性
- 📜 避免死锁 如果锁不是可重入的,那么调用一个已经获取了锁的方法的其他同步方法时,会导致死锁。
- 📜 提高封装性 可重入性允许在一个同步方法中调用另一个同步方法。
📜 示例分析
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
- 在上述代码中,
add
方法是同步的,所以当线程进入这个方法时,它会获取this
对象的锁。 - 当
n < 0
时,add
方法调用dec
方法。由于dec
方法也是同步的,它也需要this
对象的锁。 - 由于Java中的
synchronized
锁是可重入的,所以线程可以再次获取this
对象的锁,并进入dec
方法,而不会发生死锁。
📝 总结
- Java中的
synchronized
锁是可重入的,这意味着一个线程可以多次获取同一把锁。 - 可重入性是为了避免死锁并提高封装性。
- ⚠️ 注意: 当一个线程获取了锁后,JVM会记录这个锁的重入次数。只有当重入次数减少到0时,锁才会真正被释放,其他线程才能获取该锁。
🌟 死锁 (Deadlock) 和如何避免
📌 死锁的定义
- 当两个或多个线程互相等待对方释放锁,并永远地等待下去,这种情况称为死锁。
📌 死锁的条件
- 📜 互斥条件 一个资源每次只能被一个线程使用。
- 📜 请求与保持条件 一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 📜 不剥夺条件 线程已获得的资源在未使用完之前不能被其他线程强行剥夺。
- 📜 循环等待条件 若干进程之间形成一种头尾相接的循环等待资源关系。
📜 示例分析
public void add(int m) {
synchronized(lockA) { ... }
synchronized(lockB) { ... }
}
public void dec(int m) {
synchronized(lockB) { ... }
synchronized(lockA) { ... }
}
- 在上述代码中,如果两个线程同时执行
add()
和dec()
,可能会发生死锁。 线程1
获得lockA
并等待lockB
,而线程2
获得lockB
并等待lockA
,造成了死锁。
🛠️ 如何避免死锁
- 避免死锁的基本方法是确保所有线程都以相同的顺序获得锁。
- 在上述示例中,我们可以通过始终首先获得
lockA
,然后获得lockB
来避免死锁。
public void dec(int m) {
synchronized(lockA) { ... }
synchronized(lockB) { ... }
}
📝 总结
- 死锁是多线程编程中的一个严重问题,一旦发生,只能强制结束进程。
- 避免死锁的关键是确保所有线程都以相同的顺序获得锁。
- ⚠️ 注意: 在设计多线程程序时,始终要考虑死锁的可能性,并采取预防措施。
🌟 ReentrantLock
in Java
📌 ReentrantLock
的基本概念
ReentrantLock
是一个实现了Lock
接口的类,与传统的synchronized
锁机制相比,提供了更高的加锁和解锁的灵活性。
📜 为什么选择 ReentrantLock
- 📜 可重入性 和
synchronized
一样,一个线程可以多次获取同一个锁。 - 📜 尝试获取锁 提供了
tryLock()
方法,可以设置等待锁的时间,从而避免无限期的等待。 - 📜 中断获取锁 可以中断等待锁的线程。
- 📜 公平锁 可以设置为公平锁,这样锁会在等待时间最长的线程中按顺序分配。
📜 如何使用 ReentrantLock
- 使用
ReentrantLock
的基本步骤是:- 🔐 创建一个
ReentrantLock
实例。 - 🔐 在逻辑代码前调用
lock()
方法来获取锁。 - 🔐 在
finally
块中调用unlock()
方法来释放锁。
- 🔐 创建一个
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
- 在尝试获取锁时,可以使用
tryLock()
方法,并设定一个最大等待时间。如果在这个时间内没有获取到锁,该方法将返回false
。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
📝 总结
ReentrantLock
提供了比synchronized
更强大和灵活的锁定机制。- 通过
tryLock
,可以避免线程的死锁。 - 使用
ReentrantLock
时,要确保始终在finally
块中释放锁,以避免出现死锁。 - ⚠️ 注意: 虽然
ReentrantLock
提供了更多的功能,但在某些情况下,简单的synchronized
可能更适合。选择哪种锁取决于具体的用例。
🌟 使用 ReentrantLock
和 Condition
替代 synchronized
, wait
和 notify
📌 基本概念
- 在多线程编程中,当某个条件不满足时,线程可能需要等待,直到该条件变为真。这是通过
synchronized
关键字配合wait()
和notify()/notifyAll()
方法来实现的。 ReentrantLock
配合Condition
提供了一种更加灵活的方式来实现线程的等待和唤醒。
📜 如何使用 Condition
- 📜 创建
Condition
通过Lock
对象的newCondition()
方法来创建。 - 📜 等待 使用
Condition
的await()
方法来使线程等待。 - 📜 唤醒 使用
Condition
的signal()
或signalAll()
来唤醒等待的线程。
🔍 示例代码
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
📜 特点和优势
- 📜 灵活性
Condition
提供了比wait
和notify
更灵活的线程等待/唤醒机制。 - 📜 多条件 一个
Lock
可以绑定多个Condition
,这让你可以在不同的情况下进行不同的等待。 - 📜 可定时的等待 与
tryLock
类似,await
可以设置一个最大等待时间。
📝 总结
- 使用
ReentrantLock
和Condition
可以提供比synchronized
,wait
, 和notify
更强大和灵活的线程同步机制。 - 与
synchronized
关键字不同,当使用ReentrantLock
和Condition
时,你有责任手动锁定和解锁。 - ⚠️ 注意: 在实际应用中,选择合适的同步机制取决于特定的需求和场景。
🌟 使用 ReadWriteLock
提高并发读效率
📌 概念简介
-
在多线程并发读写场景下,很多时候读操作的频率远远大于写操作。使用传统的锁,如
synchronized
或ReentrantLock
,会导致所有的读和写操作都是串行的,从而降低了系统的整体性能。 -
ReadWriteLock
是一种特殊的锁,它区分了读操作和写操作,允许多个线程同时进行读操作,但在写操作时,所有的读和写操作都会被阻塞,直到写操作完成。
📜 使用方法
- 📜 创建
ReadWriteLock
使用ReentrantReadWriteLock
类来创建。 - 📜 获取读锁 使用
readLock()
方法。 - 📜 获取写锁 使用
writeLock()
方法。
🔍 示例代码
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
📜 特点和优势
- 📜 并发读 允许多个线程同时进行读操作。
- 📜 独占写 当有线程进行写操作时,不允许其他线程进行任何读或写操作。
- 📜 性能优化 在读多写少的场景中,
ReadWriteLock
可以提供比传统锁更好的性能。
📝 总结
ReadWriteLock
是一种特殊的锁机制,用于提高在读多写少的场景中的性能。- 使用
ReadWriteLock
时,要确保正确地获取和释放读锁和写锁。 - 读锁允许多线程并发读,但在有线程持有写锁时不允许任何其他的读和写操作。
- 写锁保证了写操作的独占性。
- 使用
ReadWriteLock
可以提高并发性,但也可能增加编程的复杂性,因此,在使用时要特别小心。
🌟 使用 StampedLock
提高并发读效率
📌 概念简介
-
StampedLock
是 Java 8 中引入的一种新的锁机制,它主要解决了ReadWriteLock
的一些局限性。与ReadWriteLock
不同的是,StampedLock
支持 “乐观读”,这种读取方式允许一个线程在读取数据时,其他线程可以进行写入。 -
乐观读意味着在大部分情况下,读操作不需要等待写操作完成,从而大大提高了并发效率。但是,这也带来了数据一致性的问题,因为在乐观读的过程中,可能会遇到其他线程正在写入的情况。为了处理这种情况,
StampedLock
提供了一个validate()
方法来检查在读取过程中是否发生了写操作。
📜 使用方法
- 📜 创建
StampedLock
使用StampedLock
类来创建。 - 📜 获取写锁 使用
writeLock()
方法。 - 📜 乐观读 使用
tryOptimisticRead()
方法。 - 📜 验证乐观读 使用
validate()
方法检查在读取过程中是否发生了写操作。 - 📜 获取悲观读锁 如果乐观读失败,使用
readLock()
方法。
🔍 示例代码
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock();
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
📜 特点和优势
- 📜 并发读 允许多个线程同时进行读操作。
- 📜 独占写 当有线程进行写操作时,不允许其他线程进行任何读或写操作。
- 📜 乐观读 允许一个线程在读取数据时,其他线程可以进行写入。
- 📜 数据一致性 通过
validate()
方法确保数据在读取过程中的一致性。
📝 总结
StampedLock
是一种特殊的锁机制,用于提高在读多写少的场景中的性能。- 使用
StampedLock
时,要确保正确地获取和释放读锁和写锁。 - 通过乐观读和悲观读的结合,
StampedLock
能够实现高效的并发读取,同时保证数据的一致性。 - 但是,使用
StampedLock
会增加编程的复杂性,并且StampedLock
是不可重入的,所以使用时需要特别小心。
🌟 使用 Semaphore
进行线程并发限制
📌 概念简介
-
Semaphore
是一个用于管理许可的类,它可以限制对受限资源的并发访问数量。 -
通过控制许可的数量,可以实现对资源的并发访问限制。例如,如果
Semaphore
初始化为3,则最多只允许3个线程同时访问受限资源。 -
它常常被用于资源池,如数据库连接池,线程池等。
📜 使用方法
-
📜 创建
Semaphore
使用Semaphore
构造函数并指定许可数量。 -
📜 获取许可 使用
acquire()
方法。如果所有许可都被其他线程占用,则当前线程会阻塞直到获取到许可。 -
📜 释放许可 使用
release()
方法。释放许可后,其他正在等待的线程可能会获得许可并继续执行。 -
📜 尝试获取许可 使用
tryAcquire()
方法。它可以指定等待时间,如果在指定时间内无法获取许可,它将返回false
。
🔍 示例代码
public class DatabaseConnections {
// 假设最多允许100个数据库连接
private final Semaphore semaphore = new Semaphore(100);
public Connection getConnection() throws InterruptedException {
semaphore.acquire();
try {
// 获取数据库连接
return createNewConnection();
} finally {
semaphore.release();
}
}
private Connection createNewConnection() {
// 创建新的数据库连接...
return new Connection();
}
}
📜 特点和优势
-
📜 灵活的资源访问控制 通过调整许可的数量,可以轻松地控制对资源的并发访问。
-
📜 资源管理 对于资源有限的场景,例如数据库连接,线程,文件句柄等,
Semaphore
提供了有效的管理机制。 -
📜 提高系统稳定性 防止过多的并发请求压垮系统。
📝 总结
-
Semaphore
是一个用于管理许可的并发工具,它允许多个线程访问一个或多个受限资源。 -
通过控制许可的数量,可以限制对资源的并发访问,从而有效地管理和控制资源。
-
使用
Semaphore
时,需要注意合理设置许可的数量,并确保在使用资源后正确释放许可,以便其他线程可以获取并使用资源。