更多特训营笔记详见个人主页【面试鸭特训营】专栏
250122
1. 10. 说说 AQS 吧?
AQS 的工作原理
- AQS 通过维护一个共享状态
state
和一个先进先出FIFO
的等待队列,来管理线程对共享资源的访问。 state
用volatile
修饰,用于表示当前资源的状态。在独占锁中,state
为 0 表示已被占用,为 1 表示未被占用。FIFO
等待队列底层是一个双向链表,节点包含线程的引用、等待状态及前驱和后继结点的指针。- 当线程无法获取同步器的资源时,会被加入到这个队列中。被唤醒之后线程会以 FIFO 顺序从队列中出队。
举例说明
- 卫生间只有 5 个坑位,现在有 5 个人要上厕所。
- 5 个人开始排队【FIFO 等待队列】进入坑位,状态值【state】都变成了 0 (已被占用)。
- 第 6 个人来的时候,发现没有空的坑位了【state 都是 0】,只能在外面排队等候。
- 当第 1 个人上完厕所之后,坑位的状态值变成了 1 (未被占用),此时第 6 个人进到坑位【从 FIFO 出队】。
常见实现类
AQS 常见的实现类有 ReentrantLock
、CountDownLatch
、Semaphore
等等。
2. 11. Synchronized 和 ReentrantLock 有什么区别?
Synchronized
特点
Synchronized
是 Java 内置的关键字,实现基本的同步机制,不支持超时、非公平、不可中断、不支持多条件。Synchronized
修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。synchronized
用于实现线程同步,确保同一时刻只有一个线程能访问资源。synchronized
可以用于修饰实例方法,静态方法,静态代码块。synchronized
能同时保证原子性、有序性和可见性。synchronized
具有可重入性,是可重入锁。-
- 可重入性是指:一个线程可以再次请求自己持有对象锁的临界资源。
- 每获取一次锁,计数器加一,释放锁时,计数器减一。直到计数器为 0 时,锁才会真正释放。
synchronized
是非公平锁。-
- 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到。
-
-
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:cpu多次唤醒队列中的阻塞线程导致开销较大。
-
-
- 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
-
-
- 优点:减少cpu对阻塞线程的唤醒次数,开销较少。
- 缺点:可能会有部分线程长时间无法获取资源导致被饿死在队列。
-
锁的升级过程
- 偏向锁
-
- 在没有锁竞争的情况下,锁总是”偏向“于第一个获得它的线程。
- 把当前锁偏向于某个线程,就是通过CAS机制来修改偏向锁的标记。
- 若成功竞争到偏向锁,说明加锁成功,直接返回即可。
- 若竞争偏向锁失败,说明当前已经有其他进程占用了偏向锁,就需要将锁升级到轻量级锁。
- 轻量级锁(自旋锁)
-
- 会通过多次自旋去重试竞争锁。
- 在轻量级锁的状态下,竞争锁的线程会根据自适应的自旋次数,去尝试自旋占用锁资源。
- 轻量级锁自旋次数过多,或获取锁失败的次数过多时,轻量级锁变成重量级锁。
- 重量级锁
-
- 如果线程无法通过轻量级锁,JVM 会将该锁升级为重量级锁。
- 在重量级锁的状态下,没有竞争到锁的进程会被阻塞。
ReentrantLock
详解链接:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
ReentrantLock
是基于 AQS 实现的可重入锁,支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁和非公平锁、可中断、支持多条件判断。
表格对比
特性 | Synchronized | ReentrantLock |
---|---|---|
获取锁 | 自动获取 | 显式获取,调用 lock() |
释放锁 | 自动释放 | 显式释放,调用 unlock() |
可重入性 | 是 | 是 |
公平性 | 默认公平 | 支持公平锁,可设置为公平或非公平 |
中断响应 | 不响应中断 | 支持中断响应(lockInterruptibly() ) |
尝试锁 | 不支持 | 支持 tryLock() |
锁的粒度 | 方法或代码块级别 | 方法或代码块级别,但更灵活 |
条件变量 | wait() / notify() | Condition 类 |
复杂度 | 较简单 | 提供更多控制,较复杂 |
3. 12. Java 中 volatile 关键字的作用是什么?
基本概念
- volatile是Java提供的一种轻量级的同步机制,只能用于修饰变量。
可见性、有序性
- 在多线程环境下,本身不具备可见性和有序性。
- 不具备可见性是指:线程A对共享变量的修改,对于线程B来说是不可见的。
- 不具备有序性是指:为了优化性能,编译器和处理器可能会对指令进行重排序,使执行顺序与代码顺序不完全相同。
如何保证可见性
- volatile保证可见性的方案是
-
- volatile 会对其修饰的变量的写操作添加Lock前缀指令,将缓存信息写入内存。
- 缓存信息被写入内存后,会导致其他线程存储的内存地址的数据无效。
- 线程在获取变量的值时,会检查自己现在缓存信息是否被修改。
- 如果被修改,就从将自己的缓存信息更新为当前内存信息,然后再获取变量信息。
如何保证有序性
- volatile 会对其修饰的变量添加读写内存屏障,进而实现禁止指令重排序,保证其原有的顺序。
原子性
- 原子性是指:某些操作不可分割,要么全做,要么不做。
为何无法保证原子性
- 被volatile修饰的变量不具有原子性,对于某个符合操作来说(如i++),多个线程同时进行i++,无法保证两个线程的原子性。i++实际上是先取i的值,再i+1,最后把新值写回i,可能线程A获取到i的值后还没有+1就已经阻塞了,此时线程B访问i,获取的还是原值,无法保证变量i的原子性。
volatile和synchronized的区别
- volatile轻量级,开销较小,synchronized重量级,开销较大。
- volatile能保证可见性和有序性,无法保证原子性,synchronized可以保证可见性有序性和原子性。
- volatile不会造成线程的阻塞,synchronized有可能会造成线程的阻塞。
- volatile适用于一个线程写,多个线程读,如状态标志,synchronized适用于临界区,如共享资源的读写。