Java面试-锁-基础


  • synchronized(非公平锁) :CPU悲观锁(独占锁),在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候会导致效率很低
    • 对象监视器:锁非null对象,写在方法上锁this,写在静态方法上锁Class
    • 线程的状态转换:
      • 所有请求锁的线程将被首先放置到竞争队列 Contention List
      • 有资格成为候选人的线程被移到 Entry List
      • 那些调用 wait 方法被阻塞的线程被放置到 Wait Set
      • 任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
      • 获得锁的线程称为 Owner
      • 释放锁的线程 !Owner
    • Java对象:对象头(Mark Word)+实例数据+填充数据
    • 偏向锁:锁会偏向于第一个获得它的线程,持有偏向锁的线程将永远不需要进行同步操作
    • 生命周期:偏向锁(01),轻量级锁(00);重量级锁(10)
      • 对象创建 -> 是否偏向(0);锁标志位(01)
      • 临界区,CAS 将线程ID插入Markword -> 是否偏向(1);锁标志位(01)
      • 其他线程发现线程ID不指向自己 -> 如果偏向标志位是0,重新偏向;存在竞争则升级为轻量级锁(锁标志位:00)
        • 此过程需要暂停拥有偏向锁的线程,并撤销偏向锁
      • CAS 设置 Markword 锁记录指针 -> 成功则获得锁;失败则自旋(自旋锁默认10次)
        1. 线程在自己的栈桢中创建锁记录 LockRecord。
        2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
        3. 将锁记录中的Owner指针指向锁对象。
        4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。
      • 自适应自旋锁:如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,则根据线程最近获得锁的状态来调整循环次数
      • 重量级锁(互斥锁):依赖对象内部的monitor锁,所以依赖操作系统的MutexLock(互斥锁)来实现,锁标志位(10)
        • 开销大:会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu,但是阻塞或者唤醒一个线程时,就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长
        • 持有轻量级锁的线程可以继续其执行,直至它足以释放锁。
        • 当持有轻量级锁的线程执行同步块中的代码完毕,它会查看对象头标志位,发现锁已经升级为重量级锁,并遵照重量级锁的释放流程来释放锁
      • 锁降级:锁降级的情况是非常罕见并且特定的,目前并不是JVM的标准行为,一般来说都是不可逆的
        • 当JVM进入SafePoint安全点(所有用户线程都停止),会检查是否有闲置的Monitor,然后试图进行降级
        • 在垃圾收集时,JVM确实会进入安全点。不过,进入安全点并清理闲置的Monitor主要是为了回收无用资源,并非为了锁的降级。例如,在CMS垃圾回收算法中的并发阶段,就需要等待所有线程都到达安全点来进行STW(Stop-The-World)阶段的操作,但这与锁的状态无关。
      • 偏向锁的批量撤销:常发生在JVM检测到现在或未来可能存在锁竞争时,此时,它会撤销一些或全部对象的偏向状态。撤销是通过STW(Stop-The-World)暂停来完成的,以此保证对所有线程可见性和一致性。安全点期间,所有的偏向锁都会被撤销,锁状态会回滚到未锁定(无锁状态)的状态。
        1. 当另一个线程尝试获取已经被偏向某个线程的锁时,这表明偏向锁的单线程优势不再持续。在这种情况下,为了使其他线程也能获取锁,并且提高系统的整体性能指标,JVM可能会执行偏向锁的批量撤销。
        2. 具体来说,它会遍历堆中所有的对象头,去掉偏向标志并且重新设置相关lock word的状态,使得这些锁不再偏向任何线程。这通常在安全点(SafePoint,一个能保证所有线程在一个已知点暂停的时机)发生,因为进行这种撤销需要所有Java线程的协助。
        3. 偏向锁的批量撤销还可能是垃圾收集的一部分。例如,在进行垃圾回收暂停时,JVM会检查所有的锁,并撤销那些没有被任何线程持有的偏向锁。这样的撤销是批量进行的,因为它涉及许多对象和锁。

MarkWord


  • ReentrantLock: CAS + AQS (默认是非公平锁,也可以指定为公平锁)
    1. 先通过CAS尝试获取锁
    2. 如果此时锁已经被占用,该线程加入AQS队列并wait()
    3. 当前驱线程的锁被释放,挂在等待队列为首的线程就会被notify(),然后继续CAS尝试获取锁
      • 非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占
      • 公平锁,只有在等待队列头的线程才可以获取锁,新来的线程只能插入到队尾
  • CAS:操作中包含三个参数:一个内存位置 V(Variable,变量),一个期望的原值A(Assumed value,假设值),以及一个新值B(New value)
    • 如果内存位置V的当前值与期望的原值A相匹配,那么处理器会将这个内存位置上的值更新为新值B ,否则不执行更新
    • ABA问题:加版本戳,AtomicStampedReference
  • volatile : 适合一写多读
    • 可见性:禁止线程的工作内存对 volatile 修饰的变量进行缓存,synchronizedLock 通过释放锁之前将变量的修改刷新到主存来保证可见性
    • 当对volatile变量执行写操作后,JVM会把工作内存中的最新变量值强制刷新到主内存,写操作会导致其他线程中的缓存无效,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录