java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java 锁类型

Java中锁的类型详解

作者: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表示公平

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

注意事项
示例代码
// 公平锁示例
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)中,写锁是独占锁,确保写操作原子性。
注意事项

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

共享锁 (Shared lock)

共享锁(Shared Lock)是一种允许多个线程同时读取资源,但禁止写入的锁机制。与排他锁(Exclusive Lock)互斥的特性不同,共享锁适用于读多写少的场景,能有效提高并发性能。

Java中主要通过ReadWriteLock接口及其实现类ReentrantReadWriteLock实现共享锁:

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();
        }
    }
}

注意事项

共享锁与同步代码块的对比

特性共享锁(ReadWriteLock)synchronized
并发性读操作并发,写操作互斥完全互斥
灵活性可分离读/写锁单一锁机制

其他共享锁实现

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语句实现:

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关键字相比,它提供更灵活的锁操作,支持公平锁、非公平锁、可中断锁等待等特性。

核心特性

基本用法

ReentrantLock lock = new ReentrantLock(); // 非公平锁
lock.lock(); // 获取锁
try {
    // 临界区代码
} finally {
    lock.unlock(); // 确保锁释放
}

高级功能

1. 尝试获取锁
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()
性能高竞争时更优低竞争时更优

注意事项

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

不可重入锁 (Non-reentrant lock)

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

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

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

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
同一线程重复获取导致阻塞/死锁允许,记录重入次数
实现复杂度简单需维护持有线程和计数器
适用场景无嵌套锁的简单同步递归调用或复杂同步逻辑

注意事项

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

您可能感兴趣的文章:
阅读全文