synchronized

三种应用方式

synchronized 关键字最主要有以下 3 种应用方式,下面分别介绍

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized 的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。锁的升级是单向的。

偏向锁

如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁。对于没有锁竞争的场合,偏向锁有很好的优化效果

锁升级过程

  • 当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的 threadID,因为偏向锁不会主动释放锁
  • 因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致
    • 如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
    • 如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活
      • 如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;
      • 如果存活,那么立刻查找该线程(线程1)的栈帧信息
        • 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁
        • 如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量级锁

轻量级锁适合的场景是线程交替执行同步块的场合。竞争锁对象的线程不多,而且线程持有锁的时间也不长。自旋这等待锁释放

锁升级过程

  1. 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址
  2. 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁
  3. 如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

锁粗化

将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

锁消除

JVM在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁

JAVA对象头

synchronized 使用的锁对象是存储在 Java 对象头里。

在线程执行同步代码块之前,JVM会现在当前线程的栈桢中创建用于存储锁记录的空间,并将锁对象头中的 MarkWord 信息复制到锁记录中( Displaced Mard Word)。然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针

https://tva1.sinaimg.cn/large/007S8ZIlly1ggjly2shlsj30iy0913zk.jpg

monitor

synchronized 通过 monitorenter 和 monitorexit 实现。

monitor竞争

  1. 通过CAS尝试把monitor的_owner字段设置为当前线程
  2. 如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ,记录重入的次数
  3. 若之前_owner指向的BasicLock在当前线程栈上,说明当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程,该线程成功获得锁并返回
  4. 如果获取锁失败,则等待锁的释放

monitor等待

  1. 当前线程被封装成ObjectWaiter对象node
  2. 在循环中,通过CAS把node节点添加到链表
  3. 通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒

ObjectMonitor

进入wait/notify方法之前,要获取synchronized锁。

ObjectMonitor对象中有两个队列:_WaitSet_EntryList,用来保存ObjectWaiter对象列表;_owner指向获得ObjectMonitor对象的线程。

  • _WaitSet :处于wait状态的线程
  • _EntryList:处于等待锁block状态的线程

https://tva1.sinaimg.cn/large/007S8ZIlly1gh6rl1e4okj30et08vt97.jpg

object.wait()

  1. 将当前线程封装成ObjectWaiter对象node
  2. 将node添加到_WaitSet列表
  3. 释放当前的ObjectMonitor对象,这样其它竞争线程就可以获取该ObjectMonitor对象
  4. 最后底层的park方法会挂起线程

object.notify()

  1. 如果当前_WaitSet为空,即没有正在等待的线程,则直接返回
  2. 获取_WaitSet列表中的第一个ObjectWaiter节点
  3. 根据不同的策略,将取出来的ObjectWaiter节点,加入到_EntryList或自旋

流程

  1. 多个线程monitorenter时,只有一个可以获取lock对象关联的monitor,和monitor建立关联。
  2. 获取失败的线程加锁失败,进入等待队列。
  3. 获得monitor的线程执行wai时,将当前线程放入wait set,等待被唤醒,并放弃lock对象上的所有同步声明。
  4. notify方法会选择wait set中任意一个线程唤醒,notifyAll方法会唤醒monitor的wait set中所有线程。