Java中锁的类型详解
目录
- 按照锁的特性分类
- 公平性分类
- 公平锁(Fair lock)
- 非公平锁(Non-fair-lock)
- 排他性分类
- 独占锁(Exclusive lock)
- 共享锁 (Shared lock)
- 使用场景
- 注意事项
- 共享锁与同步代码块的对比
- 其他共享锁实现
- 获取方式分类
- 悲观锁(Pessimistic lock)
- 实现方式
- 数据库悲观锁
- 注意事项
- 适用场景
- 乐观锁 (Optimistic lock)
- 乐观锁的实现方式
- 适用场景
- 注意事项
- 状态分类
- 可重入锁 (Reentrant lock)
- 核心特性
- 基本用法
- 高级功能
- 与synchronized对比
- 注意事项
- 不可重入锁 (Non-reentrant lock)
- 不可重入锁的问题
- 应用场景
- 不可重入锁与可重入锁的对比
- 注意事项
按照锁的特性分类
公平性分类
公平锁(Fair lock)
公平锁指多个线程按照申请锁的顺序依次获取锁,遵循先到先得的原则,避免线程饥饿现象。在Java中,公平锁通常通过ReentrantLock或ReentrantReadwriteLock的构造函数指定公平策略实现。
ReentrantLock的公平模式
true启用公平锁:
ReentrantLock fairLock = new ReentrantLock(true); // true表示公平锁
公平锁会维护一个线程等待队列,按请求顺序分配锁,但性能略低于非公平锁。
ReentrantReadWriteLock的公平模式
ReentrantLock,通过构造函数指定公平性:
ReentrantReadWriteLock fairReadWriteLock = new ReentrantReadWriteLock(true);
读写锁的公平模式下,读锁和写锁的分配均遵循请求顺序。
Semaphore的公平模式
Semaphore fairSemaphore = new Semaphore(permits, true); // true表示公平
公平模式下,线程按申请许可证的顺序获取资源。
注意事项
- 性能权衡:公平锁减少线程饥饿但增加上下文切换开销,非公平锁吞吐量更高。
- 默认行为:
ReentrantLock和Semaphore默认是非公平锁,需显式声明公平策略。 - 适用场景:严格顺序需求或避免饥饿时使用公平锁,高并发场景优先考虑非公平锁。
示例代码
// 公平锁示例
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
非公平锁(Non-fair-lock)
它不保证线程获取锁的顺序与请求锁的顺序一致。线程可以在锁被释放时直接尝试获取锁,而不考虑是否有其他线程已经在等待队列中。
实现方式
在Java中,ReentrantLock类默认使用非公平锁策略。可以通过以下代码显式创建非公平锁:
ReentrantLock lock = new ReentrantLock(false); // false表示非公平锁
特点
非公平锁允许新请求锁的线程插队,即使有其他线程在等待队列中。这种机制减少了线程切换的开销,提高了吞吐量,但可能导致某些线程长时间无法获取锁。
优点
- 减少线程切换,提高性能
- 在高并发场景下吞吐量更高
缺点
- 可能导致某些线程饥饿
- 无法保证公平性
使用场景
- 锁持有时间较短
- 线程竞争不激烈
- 对吞吐量要求高于公平性要求
与公平锁的对比
// 非公平锁 ReentrantLock nonFairLock = new ReentrantLock(false); // 公平锁 ReentrantLock fairLock = new ReentrantLock(true);
非公平锁的性能通常优于公平锁,因为减少了线程切换的开销。但在要求严格的公平性场景下,应该使用公平锁。
非公平锁的底层实现
非公平锁通过AQS(AbstractQueuedSynchronizer)实现。当线程尝试获取锁时,会先尝试直接获取,失败后才进入等待队列:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setE编程xclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
排他性分类
独占锁(Exclusive lock)
独占锁(Exclusive Lock)是一种同步机制,同一时间只允许一个线程持有锁,其他线程必须等待锁释放后才能获取。Java 中主要通过 synchronized 关键字和 ReentrantLock 类实现独占锁。
使用synchronized实现独占锁
synchronized 是 Java 内置的独占锁机制,可以修饰方法或代码块。
方法级别锁
public synchronized void exclusiveMethod() {
// 临界区代码
}
代码块级别锁
public void exclusiveblock() {
synchronized (this) {
// 临界区代码
}
}
特点
- 自动释放锁:线程执行完同步代码或发生异常时,锁会自动释放。
- 可重入性:同一线程可重复获取已持有的锁。
使用ReentrantLock实现独占锁
ReentrantLock 是 java.util.concurrent.locks 包下的显式锁实现,提供更灵活的锁控制。
基本用法
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 确保锁释放
}
}
高级功能
可中断锁
lock.lockInterruptibly(); // 响应中断的锁获取
尝试获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试在指定时间内获取锁
try {
// 临界区代码
} finally {
lock.unlock();
}
}
公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
应用场景
- 资源互斥访问如单例模式的双重检查锁、共享变量的线程安全操作。
- 写操作保护在读写锁(
ReadWriteLock)中,写锁是独占锁,确保写操作原子性。
注意事项
- 避免死锁:确保锁的获取和释放成对出现,尤其是异常场景。
- 性能考量:高并发场景下,
ReentrantLock的灵活性可能优于synchronized,但需手动管理锁释放。
通过合理选择 synchronized 或 ReentrantLock,可以高效实现线程安全的独占访问控制。
共享锁 (Shared lock)
共享锁(Shared Lock)是一种允许多个线程同时读取资源,但禁止写入的锁机制。与排他锁(Exclusive Lock)互斥的特性不同,共享锁适用于读多写少的场景,能有效提高并发性能。
Java中主要通过ReadWriteLock接口及其实现类ReentrantReadWriteLock实现共享锁:
- 读锁(共享锁):通过
readLock()方法获取,允许多个线程同时持有。 - 写锁(排他锁):通过
writeLock()方法获取,同一时间仅允许一个线程持有。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); // 共享锁 ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); // 排他锁
使用场景
- 缓存系统:多个线程可并发读取缓存数据,写入时需独占。
- 资源池管理:如数据库连接池的读取操作。
// 示例:使用共享锁实现线程安全的缓存
class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public V get(K key) {
rwLock.readLock().lock();
try {
rwww.devze.cometurn map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
注意事项
- 锁升级问题:持有读锁时尝试获取写锁会导致死锁,需先释放读锁。
- 公平性选择:
ReentrantReadWriteLock支持公平/非公平模式,非公平模式吞吐量更高。
共享锁与同步代码块的对比
| 特性 | 共享锁(ReadWriteLock) | synchronized |
|---|---|---|
| 并发性 | 读操作并发,写操作互斥 | 完全互斥 |
| 灵活性 | 可分离读/写锁 | 单一锁机制 |
其他共享锁实现
- StampedLock:Java 8引入,支持乐观读锁,适用于读操作远多于写的场景。
StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead(); // 乐观读锁
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock(); // 退化为悲观读锁
}
获取方式分类
悲观锁(Pessimistic lock)
悲观锁是一种并发控制机制,假设多线程并发访问共享资源时大概率会发生冲突,因此在访问数据前会先加锁,确保其他线程无法同时修改。适用于写操作频繁的场景。
实现方式
synchronized 关键字
通过synchronized修饰方法或代码块,实现隐式锁:
public synchronized void updateData() {
// 临界区代码
}
或使用代码块锁定特定对象:
public void updateData() {
synchronized (this) { // 锁住当前对象
// 临界区代码
}
}
ReentrantLock
java.util.concurrent.locks.ReentrantLock提供更灵活的显式锁:
private final ReentrantLock lock = new ReentrantLock();
public void updateData() {
lock.lock(); // 手动加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放
}
}
数据库悲观锁
在JDBC中可通过SQL语句实现:
- SELECT ... FOR UPDATE(mysql/oracle):
Connection conn = ...;
try {
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM accounts WHERE id = ? FOR UPDATE"
);
ps.setInt(1, accountId);
ResultSet rs = ps.executeQuery();
// 修改数据后提交
conn.commit();
} catch (SQLException e) {
conn.rollback();
}
注意事项
- 死锁风险:多个线程互相持有对方所需锁时会导致死锁,需设计合理的加锁顺序。
- 性能开销:频繁加锁可能降低系统吞吐量,读多写少的场景建议考虑乐观锁。
- 锁粒度:尽量缩小锁范围(如锁定行而非整表)以减少阻塞。
适用场景
- 数据竞争激烈的写操作。
- 需要保证强一致性的业务逻辑(如支付系统扣款)。
乐观锁 (Optimistic lock)
乐观锁是一种并发控制机制,假设多线程操作共享资源时不会发生冲突,因此在操作前不加锁,而是在提交更新时检查资源是否被其他线程修改。如果未被修改,则提交成功;否则,根据策略(重试、报错等)处理冲突。乐观锁适用于读多写少的场景,减少锁竞争的开销。
乐观锁的实现方式
1. 版本号机制
在数据表中增加一个版本号字段(如version),每次更新时比对版本号。若版本号匹配,则更新数据并递增版本号;否则视为冲突。
示例代码(基于数据库)
// 假设有一个实体类
public class Product {
private Long id;
private String name;
private int version; // 乐观锁版本号
}
// 更新逻辑
@Transactional
public void updateProduct(Long id, String newName) {
Product product = productDao.selectById(id);
product.setName(newName);
int updated = productDao.updateWithVersion(product);
if (updated == 0) {
throw new OptimisticLockException("更新编程客栈失败,数据已被修改");
}
}
对应的 SQL 语句示例:
UPDATE product SET name = #{newName}, version = version + 1
WHERE id = #{id} AND version = #{oldVersion};
2. CAS(Compare-And-Swap)
通过原子操作(如AtomicInteger)实现乐观锁,适用于单机或多线程环境。
示例代码(基于 AtomicInteger)
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
优点
- 无锁竞争,提高吞吐量。
- 避免死锁问题。
缺点
- 冲突频繁时需重试,可能降低性能。
- 不保证操作原子性,需结合事务或其他机制。
适用场景
- 读多写少的高并发场景(如商品库存、点赞计数)。
- 冲突概率较低的业务逻辑。
注意事项
- 版本号需为整型或时间戳,确保可比较性。
- 分布式环境中需结合分布式锁或数据库唯一约束。
状态分类
可重入锁 (Reentrant lock)
可重入锁(ReentrantLock)是Java中一种显式锁机制,属于java.util.concurrent.locks包。与synchronized关键字相比,它提供更灵活的锁操作,支持公平锁、非公平锁、可中断锁等待等特性。
核心特性
- 可重入性
- 同一线程可以多次获取同一把锁,避免死锁。每次获取锁后需对应释放,通常通过计数器实现。
- 公平性选择
- 通过构造函数指定公平锁(
fair=true)或非公平锁(默认)。公平锁按请求顺序分配,非公平锁允许插队。 - 条件变量(Condition)
- 通过
newCondition()创建多个条件队列,实现精细化的线程等待/唤醒机制,类似wait()和notify()。
基本用法
ReentrantLock lock = new ReentrantLock(); // 非公平锁
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 确保锁释放
}
高级功能
1. 尝试获取锁
tryLock():立即返回是否成功获取锁。tryLock(long timeout, TimeUnit unit):在指定时间内尝试获取锁。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 操作临界区
} finally {
lock.unlock();
}
} else {
// 处理超时逻辑
}
2. 可中断锁
lockInterruptibly()允许在等待锁时响应中断,避免死等。
try {
lock.lockInterruptibly();
// 临界区代码
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3. 公平锁示例
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
fairLock.lock();
try {
// 公平锁保护的代码
} finally {
fairLock.unlock();
}
与synchronized对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 显式调用lock()/unlock() | 隐式(代码块/方法) |
| 公平性 | 支持配置 | 非公平 |
| 可中断 | 支持 | 不支持 |
| 条件变量 | 支持多个Condition | 单一wait()/notify() |
| 性能 | 高竞争时更优 | 低竞争时更优 |
注意事项
- 必须在
finally块中释放锁,避免异常导致锁泄漏。 - 避免嵌套过多锁操作,可能导致逻辑复杂化。
- 公平锁可能降低吞吐量,需根据场景权衡。
通过合理使用ReentrantLock,可以更灵活地控制多线程并发,尤其适用于需要复杂同步策略的场景。
不可重入锁 (Non-reentrant lock)
不可重入锁(Non-Reentrant javascriptLock)是一种线程同步机制,特点是同一线程在持有锁的情况下,若再次尝试获取该锁,会导致线程阻塞或死锁。与可重入锁(如 ReentrantLock)不同,不可重入锁不记录持有线程的重复获取次数。
不可重入锁通常通过以下方式实现:
- 锁状态标记:使用一个布尔变量(如
isLocked)表示锁是否被占用。 - 线程检查:获取锁时,若锁已被占用(无论是否当前线程持有),均会阻塞。
以下是一个简单的不可重入锁实现示例:
public class NonReentrantLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
while (isLocked) {
wait(); // 若锁被占用,当js前线程等待
}
isLocked = true; // 获取锁
}
public synchronized void unlock() {
isLocked = false;
notify(); // 唤醒等待线程
}
}
不可重入锁的问题
死锁风险:若线程在持有锁时重复调用 lock(),会导致自身阻塞。
NonReentrantLock lock = new NonReentrantLock(); lock.lock(); lock.lock(); // 线程在此处永久阻塞
灵活性不足:无法支持递归调用或嵌套同步代码块。
应用场景
- 简单同步需求:仅需基础互斥且无嵌套锁的场景。
- 资源限制:明确要求防止同一线程重复获取锁的情况。
不可重入锁与可重入锁的对比
| 特性 | 不可重入锁 | 可重入锁(如 ReentrantLock) |
|---|---|---|
| 同一线程重复获取 | 导致阻塞/死锁 | 允许,记录重入次数 |
| 实现复杂度 | 简单 | 需维护持有线程和计数器 |
| 适用场景 | 无嵌套锁的简单同步 | 递归调用或复杂同步逻辑 |
注意事项
- 避免在不可重入锁保护的代码中调用可能再次获取锁的方法。
- 若需嵌套锁,应使用
ReentrantLock或synchronized(Java 内置的可重入锁)。
到此这篇关于Java中锁的类型详解的文章就介绍到这了,更多相关java 锁类型内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
加载中,请稍侯......
精彩评论