并发锁
并发锁是 Java 并发编程中用于解决多线程竞争资源的主要机制之一。锁的作用是确保在同一时间只有一个线程能够访问共享资源,从而避免数据不一致的问题。Java 提供了多种类型的锁来应对不同的并发场景。
# 1. ReentrantLock(可重入锁)
- 概念:
ReentrantLock
是 Java 提供的一种显式锁,相比于synchronized
,它提供了更多的锁控制能力,如尝试锁、定时锁、可中断锁等。 - 特性:
- 可重入性:
ReentrantLock
是可重入的,意味着同一个线程可以多次获取同一个锁而不会发生死锁。 - 公平性:可以通过构造方法选择公平锁和非公平锁。公平锁按请求顺序获取锁,非公平锁可以提高吞吐量,但可能会导致线程饥饿。
- 灵活性:可以使用
tryLock()
尝试获取锁,lockInterruptibly()
响应中断,提供了比synchronized
更灵活的锁控制。
- 可重入性:
示例代码:
ReentrantLock lock = new ReentrantLock();
Runnable worker = () -> {
try {
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(2000);
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
} else {
System.out.println(Thread.currentThread().getName() + " could not acquire the lock.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int i = 0; i < 3; i++) {
new Thread(worker).start();
}
在这个例子中,线程尝试获取锁,如果成功则执行相应操作,最后释放锁。
# 2. ReadWriteLock(读写锁)
- 概念:
ReadWriteLock
是一种特殊类型的锁,用于提高并发性。它包含两个锁——一个读锁和一个写锁。 - 特性:
- 读锁共享:多个线程可以同时获取读锁,因此允许多个读操作并行进行。
- 写锁独占:写锁是独占的,只有当没有线程持有读锁或写锁时,线程才能获取写锁。
- 应用场景:适用于读多写少的场景,可以大幅提高并发性能。
示例代码:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Runnable readTask = () -> {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " is reading.");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
};
Runnable writeTask = () -> {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " is writing.");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
};
new Thread(readTask).start();
new Thread(readTask).start();
new Thread(writeTask).start();
在这个例子中,多个线程可以同时获取读锁,但写锁需要独占访问。
# 3. StampedLock
- 概念:
StampedLock
是 Java 8 引入的一种改进版的读写锁,提供了乐观读锁以进一步提升并发性能。 - 特性:
- 乐观读锁:
StampedLock
提供了一种乐观读锁,允许读线程在不阻塞写操作的情况下读取数据,从而提高性能。 - 悲观读锁和写锁:也支持传统的悲观读锁和写锁,与
ReadWriteLock
类似。
- 乐观读锁:
- 使用场景:适用于读操作频繁,但写操作较少的场景,可以提高并发读的性能。
示例代码:
StampedLock stampedLock = new StampedLock();
Runnable readTask = () -> {
long stamp = stampedLock.tryOptimisticRead();
try {
System.out.println(Thread.currentThread().getName() + " is trying optimistic read.");
Thread.sleep(1000);
if (!stampedLock.validate(stamp)) {
System.out.println(Thread.currentThread().getName() + " optimistic read failed, acquiring read lock.");
stamp = stampedLock.readLock();
}
System.out.println(Thread.currentThread().getName() + " read completed.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
stampedLock.unlockRead(stamp);
}
};
new Thread(readTask).start();
new Thread(readTask).start();
在这个例子中,线程尝试乐观读取数据,如果在读取期间数据被修改,则会升级为悲观读锁。
# 4. Semaphore(信号量)
- 概念:
Semaphore
用于控制同时访问某一特定资源的线程数量,通过许可证机制来实现对资源的并发控制。 - 应用场景:适用于限制对有限资源的访问,例如限制同时访问数据库连接的线程数。
示例代码:
Semaphore semaphore = new Semaphore(2);
Runnable worker = () -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " acquired a permit.");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " released a permit.");
}
};
for (int i = 0; i < 5; i++) {
new Thread(worker).start();
}
在这个例子中,最多允许两个线程同时执行任务,其他线程会等待许可证的释放。
# 5. Lock 与 Synchronized 的比较
- 显式与隐式:
synchronized
是隐式的,不需要手动释放锁,Lock
需要显式地获取和释放锁。 - 功能:
Lock
提供了更多的功能,如可中断锁、定时锁、公平锁等,而synchronized
的功能相对简单。 - 性能:在某些场景下,
Lock
比synchronized
性能更好,特别是在竞争激烈时。
# 6. 总结
并发锁是 Java 并发编程中的重要工具,通过合理使用 ReentrantLock
、ReadWriteLock
、StampedLock
和 Semaphore
,可以有效地解决线程竞争问题,确保共享资源的安全访问。每种锁都有其适用的场景,开发者需要根据具体的需求选择合适的锁,以达到最佳的并发性能和线程安全。理解并发锁的使用和原理,有助于在高并发场景中编写更加健壮的代码。
上次更新: 2024/11/01, 13:45:14