Java锁 从乐观锁和悲观锁开始讲 面试复盘

目录

面试复盘

[Java 中的锁 大全](#Java 中的锁 大全)

悲观锁

专业解释

自我理解

乐观锁

专业解释

自我理解

悲观锁的调用

乐观锁的调用

[synchronized和 ReentrantLock的区别](#synchronized和 ReentrantLock的区别)

相同点

区别

详细对比

总结


面试复盘

Java 中的锁 大全

悲观锁

专业解释

适合写操作多的场景

先加锁可以保证写操作时数据正确

显式的锁定之后再操作同步资源

自我理解

悲观锁认为自己使用数据的时候一定有其他线程来修改数据

因此在获取数据的时候会选择先加锁

确保数据不会被别的线程修改

synchronized 和 Lock 的实现类都是悲观锁

乐观锁

专业解释

适合读操作多的场景

不加锁的特点能够使其读操作的性能大幅提升

乐观锁则能直接去操作同步资源

是一种无锁算法

自我理解

乐观锁认为自己在操作数据的时候不会有别的线程修改数据,所以不会加锁,所以他只会在自己操作数据的时候检查是否有其他线程修改更新的这个数据。

如果乐观锁去操作数据,这个数据没有更新的话。当前线程会直接将修改成功的数据写入,如果数据已经被其他线程更新了。要通过不同的实现方式进行不同操作。乐观锁在Java中是通过使用无锁编程来实现的,常用的是CAS算法。

Java原子类中的递增操作就是用CAS 自旋完成的

悲观锁的调用

复制代码
import java.util.concurrent.locks.ReentrantLock;

public class OptiPessLockDemo {
    // 悲观锁的调用方式
    public  synchronized void m1(){
        // 加锁后的业务逻辑...
    }

    // 保证多个线程使用的是同一个lock对象的前提下
    ReentrantLock lock=new ReentrantLock();

    public void m2(){
        lock.lock();
        try {
            // 操作同步资源
        }finally {
            lock.unlock();
        }
    }
    // 两个都是悲观锁
}

假设在任何时候都可能发生冲突,因此,线程必须显式地获取锁以确保数据一致性和线程安全,直到它执行完毕并释放锁。

对于 synchronized,在方法级别加锁时,锁是针对该对象的,保证同一时刻只有一个线程能够执行该方法。因此,当一个线程在执行 m1() 方法时,其他线程不能同时执行同一个对象上的 m1() 方法。

  • synchronized 关键字的锁粒度是方法级别的,锁住的是整个方法。在方法执行期间,其他线程不能进入这个方法。
  • synchronized 实现的是隐式加锁和解锁,不需要显式地调用 lock()unlock()
  • ReentrantLock 是 Java 中的一个显式锁,它提供了比 synchronized 更细粒度的锁控制和更多功能。
  • ReentrantLock 通过 lock.lock() 显式地加锁,调用 lock.unlock() 来释放锁。通常,这两个操作会放在 try...finally 代码块中,以确保即使出现异常,锁也能得到释放。

乐观锁的调用

复制代码
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

public class OptiPessLockDemo2 {
    // 乐观锁的调用方式
    private AtomicInteger atomicInteger = new AtomicInteger();
    atomicInteger.incrementAndGet();

    public static void main(String[] args) {
        int oldValue = atomicInteger.get();
        int newValue = oldValue + 1;
        // 如果值没有被修改,原子性操作成功
        return atomicInteger.compareAndSet(oldValue, newValue);
    }
}

这种方式在判断和更新之间,确保了只有一个线程能够成功更新值,其他线程则会重试或失败,从而保证了乐观锁的行为。

总之,乐观锁的核心是希望在操作时不加锁,直到最后验证冲突发生与否。如果有冲突,则可以通过重试、回滚等方式处理。

synchronizedReentrantLock的区别

synchronizedReentrantLock 都用于实现 悲观锁 (Pessimistic Locking),即在多线程环境中对共享资源进行加锁,以保证线程安全。尽管两者实现的目标相同,它们在使用方式、灵活性、性能等方面有一些不同。下面我们来详细分析它们的 相同点区别

相同点

  1. 线程安全
    • 两者都能确保在多线程环境下,访问共享资源时,只有一个线程能够持有锁,防止多个线程同时修改共享数据,避免数据不一致。
  1. 互斥性
    • 两者都保证了同一时刻只有一个线程能够执行被保护的代码块或方法。其他线程必须等待当前线程释放锁后才能执行。
  1. 支持重入性
    • synchronizedReentrantLock 都是可重入的(Reentrant),即同一线程可以多次获取同一个锁,而不会发生死锁。
  1. 作用范围
    • 两者都可以应用于同步方法或同步代码块,保护共享资源。

区别

|-----------|-----------------------------------------|-------------------------------------------------------|
| 特性 | synchronized | ReentrantLock |
| 实现方式 | 隐式加锁,Java 编译器在编译时自动处理。 | 显式加锁,需要手动调用 lock()unlock() 。 |
| 锁粒度 | 锁定整个方法或代码块,无法精确控制。 | 可以精确控制锁定的范围,允许更灵活的锁定操作。 |
| 性能 | 相对较低,特别是在高并发环境下,由于 JVM 的锁优化不足,可能导致性能瓶颈。 | 在高并发时,ReentrantLock 性能优于 synchronized ,尤其在锁竞争激烈时。 |
| 中断支持 | 不支持中断,线程获取锁时无法响应中断。 | 支持中断,可以使用 lockInterruptibly() 来在等待锁时响应中断。 |
| 公平性 | 非公平锁,线程不一定按照请求的顺序获取锁。 | 可以选择公平锁或非公平锁。使用构造函数 new ReentrantLock(true) 来创建公平锁。 |
| 死锁避免 | 需要小心死锁问题,synchronized 无法避免死锁。 | 通过 ReentrantLock 提供的 tryLock() 方法和超时机制可以更灵活地避免死锁。 |
| 锁释放机制 | 锁由 JVM 自动管理,方法执行完后自动释放。 | 必须手动调用 unlock() 释放锁,通常与 try...finally 语句配合使用。 |
| 可重入性 | 支持,可同一线程多次获取同一锁。 | 支持,可同一线程多次获取同一锁。 |
| 性能监控 | 无法直接获取锁的状态。 | 可以通过 getHoldCount() 获取当前线程持有锁的次数,进行监控。 |
| 锁升级 | 不支持锁的升级(无法从轻量级锁升级为重量级锁)。 | 可以通过锁的竞争情况动态升级为不同的锁类型(如偏向锁、轻量锁、重量锁)。 |


详细对比

  1. 锁的获取与释放
    • synchronized隐式加锁 ,锁的获取和释放是自动完成的。线程在执行被 synchronized 修饰的代码时,会自动获取该对象的锁,方法或代码块执行完后自动释放锁。
    • ReentrantLock显式加锁 ,必须手动调用 lock() 获取锁,必须手动调用 unlock() 释放锁。通常会配合 try...finally 语句使用,以保证在执行完业务逻辑后无论是否发生异常都能够释放锁。
  1. 中断响应
    • synchronized 在获取锁时无法响应中断。如果线程在等待锁的过程中被中断,它会继续等待,直到获取到锁。
    • ReentrantLock 提供了 lockInterruptibly() 方法,它允许在等待锁的过程中响应中断。线程在等待锁时,如果被中断,能够及时退出等待。
  1. 公平性
    • synchronized非公平锁,即任何线程在请求锁时,获取锁的顺序并不一定按照请求的顺序。可能先被调用的线程后获得锁,后调用的线程反而先获取到锁。
    • ReentrantLock 可以通过构造函数指定是否为 公平锁。如果设置为公平锁,锁会按照请求顺序分配给线程,先请求的线程会先获得锁。公平锁相对会增加一些性能开销,因此通常默认是非公平锁。
  1. 性能
    • 在低并发环境下,synchronized 的性能和 ReentrantLock 差不多,但在高并发环境下,ReentrantLock 会因为其灵活性和优化而表现得更好。
    • synchronized 锁的实现较为简单,但在高并发下可能存在 锁竞争 的问题,导致性能瓶颈。
    • ReentrantLock锁竞争 情况下提供了更多的优化方式,如通过自旋锁、CAS 等机制来减少线程的阻塞,从而提高性能。
  1. 死锁避免
    • synchronized 并没有提供直接的 API 来避免死锁。需要开发者自己通过代码设计来避免死锁问题。
    • ReentrantLock 提供了 tryLock() 方法,可以尝试获取锁,如果获取失败,可以选择放弃或重试。这种机制可以帮助开发者避免死锁或减少锁的等待时间。
  1. 锁的升级和监控
    • synchronized 锁的状态无法直接监控,只能通过 JVM 的内部调试工具进行查看。
    • ReentrantLock 提供了诸如 getHoldCount() 等方法来获取当前线程持有锁的次数,此外,还可以通过 getQueueLength() 来获取等待锁的线程数量,便于性能监控和分析。

总结

  • synchronized:适用于简单的同步需求,易于使用,代码简洁,但不够灵活,性能在高并发场景下可能有所下降。
  • ReentrantLock:功能更强大,提供了更多灵活性,如公平锁、中断响应、尝试加锁等,适用于复杂的多线程应用。但需要手动管理锁的获取与释放,使用上稍微复杂一些。

如果你的应用需求比较简单,且对性能要求不高,使用 synchronized 就足够了。如果你需要更多的控制、灵活性和对高并发场景的优化,ReentrantLock 会是更好的选择。

相关推荐
肥肥呀呀呀1 分钟前
在Flutter上如何实现按钮的拖拽效果
前端·javascript·flutter
GUIQU.5 分钟前
【每日一题 | 2025年5.5 ~ 5.11】搜索相关题
算法·每日一题·坚持
双叶8365 分钟前
(C语言)超市管理系统(测试版)(指针)(数据结构)(二进制文件读写)
c语言·开发语言·数据结构·c++
不知名小菜鸡.5 分钟前
记录算法笔记(2025.5.13)二叉树的最大深度
笔记·算法
小雅痞6 分钟前
[Java][Leetcode middle] 55. 跳跃游戏
java·leetcode
PXM的算法星球7 分钟前
使用CAS操作实现乐观锁的完整指南
开发语言
com未来11 分钟前
使用 NSSM 安装 Tomcat 11.0.6 为 Windows 服务
java·windows·tomcat
TDengine (老段)17 分钟前
基于 TSBS 标准数据集下 TimescaleDB、InfluxDB 与 TDengine 性能对比测试报告
java·大数据·开发语言·数据库·时序数据库·tdengine·iotdb
Zero10171317 分钟前
【React的useMemo钩子详解】
前端·react.js·前端框架
养军博客19 分钟前
spring boot3.0自定义校验注解:文章状态校验示例
java·前端·spring boot