使用synchronized锁可以实现代码块的原子性,内存可见性,但是不防止指令重排

synchronized实现加锁是通过在字节码中加入monitorenter和monitorexit指令,最终底层调用操作系统的同步函数实现,这些函数都涉及到用户态和内核态的切换、进程的上下文切换等,成本较高。

为了降低开销,JMV引入了偏向锁,轻量级锁和重量级锁,这三种锁对应了不同场景下synchronized加锁的方式。对象存储分为对象头,实例变量,填充字节组成,而锁存在于对象头中的MarkWord中,而MarkWord根据锁的不同,代表了不同含义,具体如下:

在32位的虚拟机中:

markword32

在64位的虚拟机中:

markword64

进入synchronized代码块,可能通过使用偏向锁-轻量级锁-重量级锁,或者轻量级锁-重量级锁(关闭偏向锁时)的方式。偏向锁可以撤销升级为轻量级锁,或者重新偏向其他线程,轻量级锁也可以升级为重量级锁,但是不能降级,这叫做锁升级。

结合一些资料,解释下偏向锁,轻量级锁和重量级锁:

偏向锁

如果是对象头中MarkWord 是否偏向锁|锁标志位 是 1|01 且线程ID为NULL,那么将使用偏向锁,线程1获取偏向锁,将MarkWord中的线程ID设置为线程1,表示其获得了锁,线程1再次获取只需检测MarkWord中线程ID是否还是线程1即代表获取了锁;

线程2获取锁,如果发现已经偏向了线程1了,那么JVM会等到一个全局的安全点(JVM实现,没有字节码在执行),暂停线程1,如果发现线程1已不再存活,那么将MarkWord设置为无锁状态(也就是线程ID设置为NULL),然后供其他线程竞争偏向锁,线程2可以继续通过CAS操作继续获取偏向锁(CAS,表示比较并交换,这里指比较线程ID,如果为NULL,然后设置为线程2的线程ID); 如果发现线程1存活,但是还是需要持有该偏向锁,则会撤销偏向锁,升级为轻量级锁(线程1持有该轻量级锁),否则如果不需要持有偏向锁了,则会设置成无锁然后偏向其他线程(线程2会竞争)

轻量级锁

轻量级锁的加锁,会在线程栈帧中建立一个锁记录(Lock Record)的空间,该空间一部分存储锁对象MarkWord的拷贝,另一部分存储指向轻量级锁的指针owner(指向拷贝的MarkWord属于的锁对象)。而轻量级锁对象的MarkWord中会存储指向该线程栈中的MarkWord拷贝的指针,该操作使用CAS原子操作替换,如果失败会自旋一定次数(说明有其他线程竞争,自旋的同时可能获取到轻量级锁的线程可能释放从而失败的线程拿到轻量级锁),然后膨胀为重量级锁(防止CPU空转)。

重量级锁

重量级锁就比较简单了,MarkWord中存储指向重量级锁的指针,重量级锁是另一个数据结构: 流程上主要分下面几步:

  1. 线程竞争未获取到锁,则进入竞争锁的队列
  2. 获取到锁则重量级锁指向该线程,释放则设置重量级锁指向NULL
  3. 如果持有锁的线程调用wait,则进入等待队列
  4. 如果持有锁的线程调用notify,则从等待队列移动一个线程到竞争锁的队列

参考文章:

https://blog.csdn.net/tongdanping/article/details/79647337

https://blog.csdn.net/lengxiao1993/article/details/81568130

https://blog.dreamtobe.cn/2015/11/13/java_synchronized/

https://www.cnblogs.com/yuxiang1/p/11305546.html

https://www.cnblogs.com/jinshuai86/p/9291033.html