Java中有哪些锁?
答:synchronized同步锁、reentrantlock锁,atomic原子锁,悲观锁乐观锁,公平锁非公平锁,自旋锁,segment分段锁,readwritelock读写锁。
1)synchronized同步锁
Java内置同步锁,用于方法或代码块,确保同一时间只有一个线程可以执行被锁的代码。这个锁是可重入的,即一个线程可以多次获取同一个锁。这个锁的粒度较粗,可能导致性能问题,尤其是在高并发场景下。
//同步方法,锁住的是当前实例对象(this)。如果是静态同步方法,则锁住的是类的Class对象。
public synchronized void synchronizedMethod() {
// 需要同步的代码
}
//同步代码块
synchronized (this) { // 锁定当前对象
// 需要同步的代码
}
// 锁定整个类
synchronized (MyClass.class) {
// 需要同步的代码
}
2)reentrantlock锁
是java.util.concurrent.locks包中提供的显式锁,是可重入的,它提供了比内置锁更灵活丰富的锁操作,例如尝试锁定(tryLock)、可中断的锁定(lockInterruptibly),也支持公平锁和非公平锁(默认是非公平锁)。
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 需要同步的代码
} finally {
lock.unlock(); // 释放锁
}
3)atomic原子锁
通过CAS无锁操作实现并发控制,AtomicInteger 提供了对整数的原子操作,适用于需要在多线程环境中安全地进行递增、递减或其他操作的场景。AtomicBoolean 提供了对布尔值的原子操作,适用于需要在多线程环境中安全地设置或检查布尔值的场景。
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
while (!locked.compareAndSet(false, true)) {
// 等待锁释放
}
}
public void unlock() {
locked.set(false);
}
}
4)悲观锁和乐观锁
悲观锁:
对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。(Java中synchronized关键字,进入就锁或Lock接口)
乐观锁:
对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-设置这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。(Java中Atomic类,AtomicInteger,使用了一种叫Compare-And-Swap的机制来实现线程安全)
两者对比:
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 锁定机制 | 使用互斥机制,锁定资源 | 不锁定资源,只在更新时检查冲突 |
| 优点 | 数据一致性高,适合高并发写操作 | 性能好,对读操作友好 |
| 缺点 | 可能导致死锁或性能瓶颈 | 如果冲突频繁,可能导致性能下降 |
| 适用场景 | 金融交易、库存管理 | 查询为主、写入较少的场景 |
5)公平锁和非公平锁
主要区别在于线程获取锁的顺序和调度方式。在Java中,ReentrantLock类提供了公平锁和非公平锁的实现。通过构造函数的参数可以指定锁的类型:公平锁new ReentrantLock(true),非公平锁new ReentrantLock(false),默认是非公平锁。
公平锁:
当一个线程请求锁时,如果锁已经被其他线程持有,请求线程会被放入一个等待队列中。当锁被释放时,等待队列中的第一个线程将优先获得锁。通常通过维护一个FIFO队列来记录线程的请求顺序。
非公平锁:
当一个线程请求锁时,它会首先尝试直接获取锁。如果获取成功,则无需进入等待队列,如果失败,线程才会被放入等待队列。某些线程可能会因为频繁的插队而长时间无法获取锁,从而导致线程饥饿。
两者对比:
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 按请求顺序获取锁 | 不保证顺序,允许插队 |
| 性能开销 | 较高,需要维护队列 | 较低,减少了队列操作 |
| 线程饥饿 | 不会发生 | 可能会发生 |
| 适用场景 | 对公平性要求高的场景,银行交易 | 对吞吐量要求高的场景,缓存日志 |
6)自旋锁
线程在尝试获取锁时不会直接进入阻塞状态,而是通过循环等待(自旋-原地徘徊等公共厕所坑位)来尝试获取锁。自旋锁适用于锁持有时间非常短的场景,避免线程上下文切换的开销。自旋锁减少了线程阻塞和上下文切换的开销,但如果锁持有时间过长,就可能导致CPU资源浪费。
7)segment分段锁
是一种将锁分段的机制,用于减少锁竞争。ConcurrentHashMap在JDK 1.7及之前版本中使用了分段锁。它将数据分成多个段(Segment),每一段都是一个哈希表,每个段都独立加锁,从而减少了锁竞争,提高并发性能。 在JDK 1.8及之后版本中,ConcurrentHashMap不再使用分段锁,而是通过CAS操作和细粒度锁实现线程安全。
8)readwritelock读写锁
允许多个线程同时读取共享资源,但写操作需要独占锁。这适用于读多写少的场景,可以提高读操作的并发性能,控制写操作的独占,确保数据一致性。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
ReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作
lock.readLock().lock();
try {
// 读取共享资源
} finally {
lock.readLock().unlock();
}
// 写操作
lock.writeLock().lock();
try {
// 修改共享资源
} finally {
lock.writeLock().unlock();
}
关注公众号:咖啡Beans
在这里,我们专注于软件技术的交流与成长,分享开发心得与笔记,涵盖编程、AI、资讯、面试等多个领域。无论是前沿科技的探索,还是实用技巧的总结,我们都致力于为大家呈现有价值的内容。期待与你共同进步,开启技术之旅。