synchronized底层实现

预备知识

  • Java对象(非数组):用来存储锁,由对象头、实例数据、对齐填充数据组成。

  • 对象头:由MarkWord、类型指针组成。32位JVM下的Markword占32位,存储的数据取决于锁的状态。
  • 初始是无锁状态。

  • 在运行期间MarkWord里存储的数据会随着锁状态的变化而变化

  • Monitor类型对象:重量级锁状态下,MarkWord里的指针指向的对象,ObjectMonitor(C++写的)对Monitor做的实现。
    • ObjectMonitor对象主要属性:
      • _count用来记录当前线程获取的锁计数
      • _WaitSet存放处于wait状态的线程
      • _EntryList存放处于等待获取锁,处于block状态的线程队列。
      • _owner指向持有ObjectMonitor对象的线程

synchronized

介绍

  • 用来修饰方法(静态方法、实例方法)、代码块
  • 常说的通过synchronized加锁就是指竞争获取对象头MarkWord重量级锁状态下指向的Monitor类型对象(ObjectMonitor),但是JDK1.6之后有了优化。
  • 可以保持原子性(加锁)、保持变量可见性(释放锁会将缓存刷新到主存)、不防止指令重排序(比如单例模式DoubleCheck还是会用到volatile防止指令重排序)、

原理

  • JDK1.6之前,在进入synchronized修饰的方法或代码块之前要先获取重量锁(指的是获取对象头指针指向的Monitor类型的对象)
    • 当修饰的是静态方法获取的是类的Class对象对应的Monitor对象
    • 当修饰的是实例方法获取的是该类的实例对象对应的Monitor对象
    • 当修饰的是代码块需要自己指定
  • 用synchronized修饰的代码块,编译阶段会在方法执行前后生成monitorenter、monitorexit指令。
    • JVM规范对于monitorenter指令描述:

      Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

      • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
      • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
      • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
    • 每一个对象都有一个Monitor对象,线程通过执行monitorenter指令尝试获取Monitor对象的拥有权
      • 如果拥有当前Monitor对象的线程数为0,则将_count++,当前线程称为Monitor对象的拥有者。
      • 如果当前线程已经拥有了此Monitor对象,则将_count++即可。
      • 如果其他线程已经拥有了此Monitor对象,则当前线程阻塞知道Monitor的计数_count==0,然后重新竞争获取锁。
    • JVM规范对于monitorexit指令描述:

      The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
      The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

    • 执行monitorexit指令的线程必须是此Monitor对象的拥有者(否则会抛java.lang.IllegalMonitorStateException异常),线程减少Monitor对象的锁计数,如果锁计数为0了,则线程不在是Monitor对象的拥有者,其他被这个Monitor对象阻塞的线程可以尝试获取Monitor。

获取重量锁过程

  • 当线程执行到monitorenter指令,会进入ObjectMonitor对象的_EntryList队列,通过CAS会将_owner指针指向当前线程,同时_count++,
  • 当前线程执行monitorexit指令,会释放持有的Monitor对象,并将_owner置为null同时_count--
  • 如果调用wait(),同上,但是会进入_WaitSet队列,等待被唤醒。(看到没:wait状态的线程在唤醒之后,还得需要获取锁④,然后执行完毕)

    锁优化

  • 原因:因为获取重量级锁过程中,比如将_owner指向当前线程调用的函数涉及到了特权指令Mutex Lock导致用户态线程和内核态线程之间进行切换,切换过程影响效率(比如需要保存当前线程的在CPU寄存器里的缓存,程序计数器的值等,并将新的线程载入到寄存器,更新PC程序计数器...)
  • JDK1.6做了优化,执行monitorenter指令时不会直接获取重量锁,而是先尝试获取偏向锁,=> 轻量锁 => 重量级锁。
  • 偏向锁相对于轻量级锁减少了CAS操作的次数,轻量级锁相对于重量级锁减少了系统调用。

获取偏向锁过程

  • 原因:大部分情况下不会存在线程竞争,而且只会有同一个线程进入临界区,为了减少同一线程获取锁带来的消耗,所以当进入临界区前不会先去获取重量锁,而是先获取偏向锁。
  • 膨胀成轻量级锁:偏向锁主要是为了解决同一个线程进入临界区,当有超过一个线程竞争偏向锁,就会膨胀为轻量级锁。
  • 获取偏向锁过程:
    • 先判断是否能开启偏向锁,如果可以 => 将偏向锁偏向线程ID用CAS(相对于轻量级锁获取和释放都需要CAS操作费时,偏向锁只有这一次)修改为当前线程ID。

获取轻量锁过程

  • 原因:在多个线程都会尝试进入临界区的情况下,多个线程只会交替进入临界区,不会存在锁竞争,为了减少重量级锁系统调用造成的消耗。
  • 膨胀成重量级锁:当多个线程同一时间都尝试获取锁,则会膨胀为重量级锁。
  • 获取轻量级锁获取过程:
    • 如果当前无锁并且不可偏向,会尝试获取轻量级锁,将MarkWord拷贝到当前线程的栈帧中的LockRecord,然后通过CAS更新MarkWord内容为指向当前线程LockRecord的指针,
  • 和偏向锁的区别:偏向锁是同一个线程多次获取锁,轻量级锁是多个线程交替获取锁。相同点是假定都不存在锁竞争。

自旋锁

  • 原因:咳咳,还是大部分情况下,线程持有锁的时间很短,当一个线程获取锁了以后,其他线程尝试获取锁就会进入阻塞状态,挂起->恢复都需要在用户态和内核态之间进行切换。此时如果让后来的线程进行自旋一段时间(for循环),在获取锁,可能就会获取,也就避免了转入内核态。
  • JDK1.6引入了自适应的自旋锁,即根据具体情况结合前面旋转的次数决定此次需要旋转的次数。
  • 优点:如果线程占用锁的时间比较短则自旋操作很有效,避免进入内核态
  • 缺点:如果线程占用锁的时间比较长则自旋操作白白耗费CPU资源,倒不如挂起。

其他

  • 当修饰方法会在常量池生成ACC_SYNCHRONIZED,当执行某个方法时会如果遇到此指令会同上...
public class Test {

    synchronized void test() {
        synchronized (this.getClass()) {}
    }
}

参考

猜你喜欢

转载自www.cnblogs.com/jinshuai86/p/9291033.html