在分析synchronized之前,我们先了解一些概念:用户态和内核态、CAS、java对象布局
平时我们所写的java程序是运行在用户空间的,因为我们的jvm对于操作系统来讲就是一个普通程序。用户空间的程序要执行读写硬盘、读写网络、读写内存等重要操作时必须经过操作系统内核来进行。
在JDK早期,Synchronized是重量级锁,每次申请锁都需要调用系统内核。需要从用户空间切换到内核空间,拿到锁后再将状态返回给用户空间。
cas(compare and swap)它指的是比较与交换,使用无锁的机制保证操作对象的原子性,说是无锁,其实它是一个自旋锁。
首先读取当前值,在计算预期的结果值,在把值修改回去的时候,要比较一下原来读出来的值和现在的值是否相等,如果相等,说明没有别的线程改动过,更新为新的值。如果不一样则说明已经别的线程改过了,这个时候,再次读取当前值,重新再来一遍。cas的底层是由汇编指令lock和cmpxchg(compare and exchange)组合在一起来支撑的。
ABA问题解决:使用version标记,每次操作version加1
openjdk提供了一个查看java对象布局的工具,在maven中引入如下依赖即可使用
org.openjdk.jol
jol-core
0.10
当我们new 了一个java对象的时候,它在jvm里面一定是占据了一块内存的,那么这个java对象在内存中的布局是什么样子的?
一个普通对象在内存中可以分成4个部分
下面我们用jol这个工具来看一下一个对象在内存中的布局
测试demo:
public class MackWordTest {
public static void main(String[] args) {
Object obj = new Object();
String print = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(print);
System.out.println("===========================================================");
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
运行结果如下:
前8个字节为mark word,后面4个字节为class pointer,然后是补齐。
给对象加上synchronized同步锁之后,我们发现mark word发生了变化,说明锁信息存在了mark word里面,给对象加锁,其实就是修改mark word。
接下来我们就来看synchronized锁升级的过程
我们要探究锁升级的过程,只需要看它mark word的变化过程就可以了
锁状态 | 25位 | 31位 | 1位 | 4bit | 1bit偏向锁位 | 2bit锁标志位 | |
无锁态(new) | unused | hashCode | unused | 分代年龄 | 0 | 0 | 1 |
锁状态 | 54位 | 2位 | 1位 | 4bit | 1bit偏向锁位 | 2bit锁标志位 | |
偏向锁 | 当前线程指针JAVA Thread | Epoch | unused | 分代年龄 | 1 | 0 | 1 |
锁状态 | 62位 | 2bit锁标志位 | |||||
轻量级锁/自旋锁 | 指向线程栈中Lock Record的指针 | 0 | 0 | ||||
重量级锁 | 指向重量级锁的指针(互斥量) | 1 | 0 | ||||
GC标记信息 | CMS过程用到的标记信息 | 1 | 1 |
锁的状态分成4中:无锁、偏向锁、轻量级锁或自旋锁、重量级锁
怎么区分锁的状态呢?看锁的标志位和偏向锁标志位就好了
首先看锁的标志位:
00代表轻量级锁,10代表重量级锁,我们看到无锁态和偏向锁的锁标志位都是01,这个时候再看偏向锁标志位,也就是看后三位,001是无锁,101是偏向锁。
表中当前线程指针JAVA Thread指向的就是当前线程ID
Lock Record:由于synchronized默认是可以重入的,每个线程上自旋锁的时候,在自己的线程栈中生成一个LockRecord对象,哪个线程能在markword里写入自己LockRecord对象的指针,就算是持有了锁。锁重入的时候再次生成一个LockRecord,这样就记录了到底锁了多少次。
锁升级过程:
一个java对象刚new出来的时候,它有可能是无锁态或者匿名偏向状态,这个时候我们再用synchronized(obj)给这个对象上锁,优先上偏向锁。
偏向锁就是说它偏向于某个线程,因为我们在日常使用锁的时候,大多数都是在一个线程,为了这一个线程要去调用系统内核kernel,太浪费资源了,所以JDK在这里进行了优化。凡是有一个线程第一次获得这把锁,就认为这把锁偏向于它,也就是不惊动操作系统内核,只需要将线程ID放入mark word里就可以了。
当我们有多个线程竞争同一个资源的时候,锁升级为轻量级锁/自旋锁
自旋锁就是多个线程去竞争同一把锁,通过CAS的方式,哪个线程能把自己的信息写入mark word,就算是持有了锁。自旋锁也不需要调用操作系统内核。
如果有特别多的线程同时去竞争一把锁,那这个时候自旋锁就会出现问题,大家想一下,我们的自旋锁是一直在做while循环,假设有10000个线程参与竞争,这个时候真正在干活的只有1个线程,剩下的9999个线程都在做while自旋,cpu资源全都浪费了。所以这种情况下需要升级成为重量级锁
在hotspot虚拟机中,重量级锁的底层实现就是ObjectMonitor(对象监视器),对于每个对象来说,在重量级锁的状态下,mark word中Lock Word指向ObjectMonitor(对象监视器)的起始地址,对象就是这样与monitor建立关联的。
每个对象实例都会有一个 monitor(对象监视器),ObjectMonitor里面有两个队列_WaitSet和_EntryList,分别记录了等待的线程和参与竞争的线程,还有一个_owner用来保存持有对象锁的线程的唯一标识,而ObjectMonitor本质上是依赖于操作系统内核的mutext lock(互斥锁)来实现的,这就需要从用户态切换到内核态。这里的mutex lock的最终实现也是lock和cmpxchg
重量级锁跟轻量级锁最大的区别在于,重量级锁经过操作系统内核的调度之后,系统内核提供一把锁的同时,还会为锁提供wait set队列,这些获取不到锁的线程都进入队列等待,什么时候获取到锁,线程才能继续执行。重量级锁需要阻塞和唤醒线程,这些操作都需要操作系统内核来帮忙,这就需要从用户态切换到内核态,所以效率较低
这里面重量级锁需要经过系统内核kernel,而偏向锁和轻量级锁在用户空间就可以完成。
下面我们来看一些细节,轻量级锁在什么情况下会升级成为重量级锁?
JDK1.6之前,轻量级锁升级成重量级锁有两个条件:
JDK1.6之后对此进行了优化,引入了自适应自旋,JDK会根据每个线程的运行情况来判断是不是要升级。
从上图中可以看到,偏向锁没有启动的时候,我们new了一个对象,它是一个普通对象,偏向锁已经启动的时候,new出来是一个匿名偏向对象。这里到底是什么意思?
首先java提供了偏向锁启动配置的参数(使用java -XX:+PrintFlagsFinal可以查看JVM可设置的参数):
偏向锁启动延迟时间: -XX:BiasedLockingStartupDelay=4000
偏向锁开关:-XX:UseBiasedLocking=true
偏向锁默认是打开的,它有一个启动延迟,默认是4秒钟。
为什么偏向锁要延迟4秒?
jvm在启动过程中是有大量的线程竞争资源的,这个时候启动偏向锁是没有意义的,所以延迟4秒,等待JVM启动。
在偏向锁已经启动的情况下,刚new的一个对象,还没有任何线程持有这把锁,这个时候没有偏向任何线程,所以是匿名偏向
在偏向锁未启动的情况下,new出来的对象就是普通对象,在偏向锁还没启动的时候,如果有竞争,这个时候就直接升级成轻量级锁。
我们先来看一下普通对象:
public static void main(String[] args) {
Object obj = new Object();
String print = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(print);
}
运行结果:
我们看到运行结果是001,也就是无锁态(对照前面锁状态表中的结果),说明这个时候new 出来是一个普通对象。
我们前面说过JVM默认是开启了偏向锁的,只不过要延迟4秒钟,在上面的demo中,new object()操作自然是远远不到4秒钟,所以这个时候偏向锁还未启动,new 出来的object对象就是一个普通对象,处于无锁态。
我们让它睡眠个5秒钟,再来看一下匿名偏向
public static void main(String[] args) throws Exception{
// 睡眠5秒
TimeUnit.SECONDS.sleep(5);
Object obj = new Object();
String print = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(print);
}
运行结果:
这个时候结果是101,表示偏向锁,new 出来的object对象,这个时候还没有任何线程持有这个对象的锁,所以是匿名偏向。
接着看下普通对象在偏向锁未启动的情况下,给对象加上synchronized,它会是什么样一个状态
public static void main(String[] args) throws Exception{
Object obj = new Object();
String print = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(print);
System.out.println("===========================================================");
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
运行结果:
我们看到这个对象由刚开始的 001(无锁态)转变到 00(轻量级锁)