Java 中的锁

发布于:2025-09-08 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. 乐观锁和悲观锁


悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized 关键字Lock 的实现类都是 悲观锁


乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

乐观锁的实现方式:①版本号;②CAS;



2. 公平锁和非公平锁

⽣活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平。

源码解读



为什么要默认是非公平锁?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

使用场景?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;

否则那就用公平锁,大家公平使用。



3. 可重入锁


可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

所以 Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。


3.1 可重入锁之 synchronized


简单的来说就是:在一个 synchronized 修饰的方法或代码块的内部调用本类的其他 synchronized修饰的方法或代码块时,是永远可以得到锁的

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

synchronized 可重入实现原理:

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。


3.2 可重入锁之 Lock


ReentrantLock 也是可重入锁。

ReentrantLock 的可重入性是通过以下机制实现的

3.2.1 核心原理

  1. 状态变量 (state)
    ReentrantLock 内部基于 AQS (AbstractQueuedSynchronizer) 实现,利用其 state 变量跟踪锁的重入次数:
    • state = 0:锁未被任何线程持有。
    • state > 0:锁被某线程持有,数值表示该线程的重入次数。
  2. 线程所有权检查
    AQS 维护一个 exclusiveOwnerThread 字段,记录当前持有锁的线程。当线程尝试获取锁时,会检查自身是否为锁的持有者。

3.2.2 获取锁流程(以非公平锁为例)

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 锁未被持有
        if (compareAndSetState(0, acquires)) { // CAS 抢锁
            setExclusiveOwnerThread(current); // 设置持有线程
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 当前线程已持有锁
        int nextc = c + acquires; // 增加重入次数
        if (nextc < 0) throw new Error("Maximum lock count exceeded");
        setState(nextc); // 更新 state
        return true;
    }
    return false; // 获取失败
}

3.2.3 释放锁流程

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread()) 
        throw new IllegalMonitorStateException(); // 非持有线程释放锁抛出异常
    
    boolean free = false;
    if (c == 0) { // 完全释放锁
        free = true;
        setExclusiveOwnerThread(null); // 清除持有线程
    }
    setState(c); // 更新 state
    return free; // 返回是否完全释放
}

3.2.4 关键点

  • 重入计数:每次重入获取锁时,state 递增;释放时递减,直到归零后其他线程才能获取。
  • 线程绑定:通过检查 exclusiveOwnerThread 确保只有持有线程能操作锁。
  • 异常处理:非持有线程尝试释放锁会抛出异常,防止非法操作。

3.2.5 总结

ReentrantLock 通过 AQS 的 state 变量记录重入次数,结合线程所有权检查,实现了可重入性。这使得同一线程可以多次安全地获取和释放锁,避免了自死锁问题,同时保持了锁状态的精确管理。



4. 死锁

略。。。



5. 写锁 读锁

ReentrantReadWriteLock



6. 自旋锁

CAS




网站公告

今日签到

点亮在社区的每一天
去签到