Java中锁的类型详解

 更新时间:2025年10月20日 16:47:22   作者:programer_33  
本文介绍了Java锁的多种分类,包括公平锁与非公平锁、独占锁与共享锁、悲观锁与乐观锁,以及可重入锁与不可重入锁,并分析了各自的实现方式、优缺点及适用场景,帮助选择适合的并发控制机制,感兴趣的朋友跟随小编一起看看吧

按照锁的特性分类

公平性分类

公平锁(Fair lock)

公平锁指多个线程按照申请锁的顺序依次获取锁,遵循先到先得的原则,避免线程饥饿现象。在Java中,公平锁通常通过ReentrantLockReentrantReadWriteLock的构造函数指定公平策略实现。

ReentrantLock的公平模式
通过构造函数传入true启用公平锁:

ReentrantLock fairLock = new ReentrantLock(true); // true表示公平锁

公平锁会维护一个线程等待队列,按请求顺序分配锁,但性能略低于非公平锁。

ReentrantReadWriteLock的公平模式
类似ReentrantLock,通过构造函数指定公平性:

ReentrantReadWriteLock fairReadWriteLock = new ReentrantReadWriteLock(true);

读写锁的公平模式下,读锁和写锁的分配均遵循请求顺序。

Semaphore的公平模式
信号量也可通过构造函数启用公平策略:

Semaphore fairSemaphore = new Semaphore(permits, true); // true表示公平

公平模式下,线程按申请许可证的顺序获取资源。

注意事项
  • 性能权衡:公平锁减少线程饥饿但增加上下文切换开销,非公平锁吞吐量更高。
  • 默认行为ReentrantLockSemaphore默认是非公平锁,需显式声明公平策略。
  • 适用场景:严格顺序需求或避免饥饿时使用公平锁,高并发场景优先考虑非公平锁。
示例代码
// 公平锁示例
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)) {
            setExclusiveOwnerThread(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实现独占锁

ReentrantLockjava.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); // 公平锁
应用场景
  1. 资源互斥访问
    如单例模式的双重检查锁、共享变量的线程安全操作。
  2. 写操作保护
    在读写锁(ReadWriteLock)中,写锁是独占锁,确保写操作原子性。
注意事项
  • 避免死锁:确保锁的获取和释放成对出现,尤其是异常场景。
  • 性能考量:高并发场景下,ReentrantLock 的灵活性可能优于 synchronized,但需手动管理锁释放。

通过合理选择 synchronizedReentrantLock,可以高效实现线程安全的独占访问控制。

共享锁 (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(); // 排他锁

使用场景

  1. 缓存系统:多个线程可并发读取缓存数据,写入时需独占。
  2. 资源池管理:如数据库连接池的读取操作。
// 示例:使用共享锁实现线程安全的缓存
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 {
            return 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对比

特性ReentrantLocksynchronized
锁获取方式显式调用lock()/unlock()隐式(代码块/方法)
公平性支持配置非公平
可中断支持不支持
条件变量支持多个Condition单一wait()/notify()
性能高竞争时更优低竞争时更优

注意事项

  • 必须在finally块中释放锁,避免异常导致锁泄漏。
  • 避免嵌套过多锁操作,可能导致逻辑复杂化。
  • 公平锁可能降低吞吐量,需根据场景权衡。

通过合理使用ReentrantLock,可以更灵活地控制多线程并发,尤其适用于需要复杂同步策略的场景。

不可重入锁 (Non-reentrant lock)

不可重入锁(Non-Reentrant Lock)是一种线程同步机制,特点是同一线程在持有锁的情况下,若再次尝试获取该锁,会导致线程阻塞或死锁。与可重入锁(如 ReentrantLock)不同,不可重入锁不记录持有线程的重复获取次数。

不可重入锁通常通过以下方式实现:

  • 锁状态标记:使用一个布尔变量(如 isLocked)表示锁是否被占用。
  • 线程检查:获取锁时,若锁已被占用(无论是否当前线程持有),均会阻塞。

以下是一个简单的不可重入锁实现示例:

public class NonReentrantLock {
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait(); // 若锁被占用,当前线程等待
        }
        isLocked = true; // 获取锁
    }
    public synchronized void unlock() {
        isLocked = false;
        notify(); // 唤醒等待线程
    }
}

不可重入锁的问题

死锁风险:若线程在持有锁时重复调用 lock(),会导致自身阻塞。

NonReentrantLock lock = new NonReentrantLock();
lock.lock();
lock.lock(); // 线程在此处永久阻塞

灵活性不足:无法支持递归调用或嵌套同步代码块。

应用场景

  • 简单同步需求:仅需基础互斥且无嵌套锁的场景。
  • 资源限制:明确要求防止同一线程重复获取锁的情况。

不可重入锁与可重入锁的对比

特性不可重入锁可重入锁(如 ReentrantLock
同一线程重复获取导致阻塞/死锁允许,记录重入次数
实现复杂度简单需维护持有线程和计数器
适用场景无嵌套锁的简单同步递归调用或复杂同步逻辑

注意事项

  • 避免在不可重入锁保护的代码中调用可能再次获取锁的方法。
  • 若需嵌套锁,应使用 ReentrantLocksynchronized(Java 内置的可重入锁)。

到此这篇关于Java中锁的类型详解的文章就介绍到这了,更多相关java 锁类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈Java方法调用的优先级问题

    浅谈Java方法调用的优先级问题

    这篇文章主要介绍了浅谈Java方法调用的优先级问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • Mybatis中collection和association的使用区别详解

    Mybatis中collection和association的使用区别详解

    这篇文章主要介绍了Mybatis中collection和association的使用区别详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • IDEA中的.iml文件和.idea文件的作用及意义

    IDEA中的.iml文件和.idea文件的作用及意义

    iml是 项目标识文件,每一个模块都有一个iml文件,存储模块的相关信息,跟eclipse的project文件是一样的功能,这篇文章主要介绍了IDEA中的.iml文件和.idea文件的作用,需要的朋友可以参考下
    2023-09-09
  • Eclipse导出安卓apk文件的图文教程

    Eclipse导出安卓apk文件的图文教程

    这篇文章主要为大家详细介绍了Eclipse导出安卓apk文件的图文教程,以图文结合的方式为大家分享了Eclipse是如何导出安卓apk文件的步骤,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • Git在IDEA中合并多个commit为一个

    Git在IDEA中合并多个commit为一个

    文章介绍了两种情况下的commit合并方法:未提交到远程分支和已经提交到远程分支,对于未提交的,通过reset和push操作即可;对于已提交的,使用rebase操作将多个commit合并为一个,然后强制push
    2025-10-10
  • IntelliJ IDEA 2020.1 EAP4 发布,重命名/更改签名新功能一览

    IntelliJ IDEA 2020.1 EAP4 发布,重命名/更改签名新功能一览

    这篇文章主要介绍了IntelliJ IDEA 2020.1 EAP4 发布,重命名/更改签名新功能,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-04-04
  • RabbitMQ 避免消息重复消费的方法

    RabbitMQ 避免消息重复消费的方法

    消费者端实现幂等性,意味着消息永远不会消费多次,即使收到了多条一样的消息,这篇文章给大家分享RabbitMQ 避免消息重复消费的方法,感兴趣的朋友一起看看吧
    2024-03-03
  • spring boot集成p6spy的最佳实践

    spring boot集成p6spy的最佳实践

    这篇文章主要介绍了spring boot集成p6spy的最佳实践-p6spy-spring-boot-starter的项目案例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-02-02
  • Java中BeanMap的使用方法

    Java中BeanMap的使用方法

    这篇文章主要介绍了Java中BeanMap的使用方法,BeanMap来源于spring 框架,可以将一般的类转为 Map 结构存储,基于Map的JavaBean视图,默认的键集是所有属性名称的联合,需要的朋友可以参考下
    2024-01-01
  • 关于SpringMVC对Restful风格的支持详解

    关于SpringMVC对Restful风格的支持详解

    Restful就是一个资源定位及资源操作的风格,不是标准也不是协议,只是一种风格,是对http协议的诠释,下面这篇文章主要给大家介绍了关于SpringMVC对Restful风格支持的相关资料,需要的朋友可以参考下
    2022-01-01

最新评论