文章目录
在jdk1.6之前,synchronized是重量级锁,在jdk1.6级以后jvm对锁的实现进行了优化,如使用偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
1、Syncronized应用
在java代码中synchronized关键字可以使用在代码块和方法中,根据Synchronized用的位置可以有以下使用场景:
分类 | 具体分类 | 被锁对象 | 伪代码 |
方法 | 静态方法 | 类对象 | //静态方法,锁住的是类对象 public synchronized static void method(){} |
实例方法 | 类的实例对象 | //实例方法,锁住的是该类的实例对象 public synchronized void method(){ } | |
代码块 | 实例对象 | 类的实例对象 | //同步代码块,锁住的是该类的实例对象 synchronized (this){ } |
class对象 | 类对象 | //同步代码块,锁住的是该类的类对象 synchronized(Test.class){} | |
任意实例对象Object | 实例对象Object | Client client=new Client(); //同步代码块,锁住的是配置的实例对象,锁对象为client synchronized(client){} |
由表可知,synchronized既可以修饰方法也可以修饰代码块,具体详见上表。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
2、JAVA对象头
说到锁,不得不说到java的对象头,对象头中包含了一些关于锁的字段。了解对象头对于学习synchronized锁有极大的帮助。
由图可以知道,在64位的hotspot虚拟机下,java的对象头包括两部分信息,第一部分是Mark Word,主要存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,偏向线程ID等内容。对象头的另外一部分就是类型指针(klass Word),虚拟机通过这个指针确定该对象是哪个类的实例。
在这里我们主要研究Mark Word,由图可以知道,给对象上锁,其实就是改变对象头的状态,各个锁的状态如图所示。
3、Synchronized的原理
3.1、什么是monitor
我们可以把它理解为一个同步工具,也可以描述为一种同步机制。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_count:用来记录该线程获取锁的次数
也就是说ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当有多个线程访问同一块同步代码块的时候,线程会线程会进入_EntryList,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor。
3.2、Synchronized修饰代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
编译上述代码并使用javap反编译后得到字节码如下:
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是monitor所对应持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
需要说明的是:monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁。
通过上面的描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
3.3、Synchronized修饰实例方法
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
查看反编译后结果:
从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成,是隐式同步,JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有monitor,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
4、线程中的start方法时如何调用run方法的
我们知道,synchronized同步锁,是离不开线程的,那么线程时如何执行的呢?
首先我们启动一个线程需要调用start方法,进入start方法,可以知道start方法会调用本地方法start0,编译openJDK源码可以知道,这个start0会执行Thread.c文件请求hotspot创建javaThread方法,最终调用操作系统函数pthread_create方法,pthread_create方法需要传入四个参数,第三个参数就是线程的主体方法(参数为java_start),在这个主体函数里面 通过某种方式调用java的run方法。
也就是说Java执行线程,最终调用操作系统函数。Java创建一个线程等于操作系统创建一个线程。
5、notify和notifyAll的区别
当一个线程进入wait之后,就必须等待其他线程notify/notifyAll,使用notifyAll可以唤醒所有处于wait状态的线程,使其重新进入锁的竞争队列中,使用notify只能唤醒一个。如果没把握,建议 notifyAll,防止 notify因为信号丢失而造成程序异常。
6、偏向锁
在学习偏向锁之前,我们先了解一下一些关于偏向锁的JVM参数设置信息。通过-XX:+PrintFlagsFinal 查看jvm默认参数,其中有这么一段:
BiasedLockingStartupDelay=4000:这是偏向锁启动延迟的参数,默认的情况下为延迟4s。
BiasedLockingBulkRdbiasThreshold=20: 这是偏向锁批量重偏向的阈值,在后面的章节中会进行说明。
BiasedLockingBulkRevokeThreshold=40:这是偏向锁批量撤销的阈值,在后面的章节中会进行说明。
我们可以通过 -XX:BiasedLockingStartupDelay=0 来关闭偏向锁延迟。通过 -XX:-UseBiasedLocking=false 关闭偏向锁,程序默认会进入轻量级锁状态。
那么为什么JVM虚拟机在启动的时候会会将偏向锁默认延迟4s呢?
因为程序执行的时候会先启动jvm、gc等线程,多个线程抢占资源,所以存在资源竞争,所以如果不取消偏向锁,就会不断的进行锁升级,消除偏向锁,这些过程会浪费资源,所以直接取消偏向锁,4s以后开启偏向锁。
偏向锁的引入
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
通过JOL获取偏向锁对象头信息,添加 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 来禁用偏向锁延迟:
public class A {
boolean flag=false;
}
public class JOLExample2 {
static A a;
public static void main(String[] args) {
a=new A();
out.println("befor lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行结果为:
我们的计算机中存储数据是以小端的方式,所谓小端模式(Little-endian), 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内在的低地址中,这种存储模式将地址的高低和数据位 权有效结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致;
因此上图中画红色框的地方就是Mark Word中后8位,这8位的后三位便是偏向锁标识(1bit)和锁的标志位(2bit)
before lock 、lock ing 和after lock的结果都为00000101,但是before lock 下没有线程持有锁,只有一个偏向状态。后面两个都有线程持有锁(都有线程id,并且相同)。也就是说在偏向锁释放锁之后,在没有其他线程获取锁的情况下,依然持有这个偏向锁。
偏向锁的获取
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
加锁和对象的hashCode的关系
我们知道,在有锁状态下,Mark Word的前56bit位置被锁指针或者线程id占用,对象的hashCode存到哪里呢,还是不存在hashCode了呢?
先看一段代码,在加锁之前计算对象hashCode值:
public class JOLExample8 {
static A a;
public static void main(String[] args) throws Exception {
//保证偏向锁已经被开启
Thread.sleep(5000);
a= new A();
a.hashCode();
out.println("befor lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行结果:
befor lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
lock ing
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 40 f2 89 02 (01000000 11110010 10001001 00000010) (42594880)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
after lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
before lock:无锁; lock ing:轻量锁 after lock:无锁。说明如果对象已经计算了hashcode就不能偏向了。
下面再看一段代码,当线程处于偏向锁状态,需要计算HashCode的情况:
public class JOLExample8 {
static A a;
public static void main(String[] args) throws Exception {
//保证偏向锁已经被开启
Thread.sleep(5000);
a= new A();
out.println("befor lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
a.hashCode();
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行结果:
befor lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
lock ing
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 13 3a 26 (00101010 00010011 00111010 00100110) (641340202)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
after lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 13 3a 26 (00101010 00010011 00111010 00100110) (641340202)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
由结果可知,lock ing的时候 锁状态为010 重量锁。因此当一个对象当前正处于偏向锁状态,并且需要计算其hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁。
下面再看一段代码,当一个对象当前正处于偏向锁状态,调用wait方法的情况:
public class JOLExample9 {
static A a;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1 = new Thread() {
public void run() {
synchronized (a) {
try {
synchronized (a) {
System.out.println("before wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
a.wait();
System.out.println(" after wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(7000);
synchronized (a) {
a.notifyAll();
}
}
}
运行结果:
befre lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 9f c1 00 f8 (10011111 11000001 00000000 11111000) (-134168161)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
before wait
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 10 5f 29 (00000101 00010000 01011111 00101001) (694095877)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 9f c1 00 f8 (10011111 11000001 00000000 11111000) (-134168161)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
after wait
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 9a 33 7d 26 (10011010 00110011 01111101 00100110) (645739418)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 9f c1 00 f8 (10011111 11000001 00000000 11111000) (-134168161)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
根据结果可知,调用wait方法后,会变成010重量锁。
总结:
- 当一个对象已经计算过hash code,它就无法进入偏向锁状态;
- 当一个对象当前正处于偏向锁状态,并且需要计算其 hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
- 当一个对象当前正处于偏向锁状态,调用wait方法则立刻变成重量锁。
- 重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储hash code的值。或者简单说就是重量锁可以存下hash code。可以通过这个链接查看ObjectMonitor对象源码:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/8c0fa90986a6/src/share/vm/runtime/objectMonitor.hpp
7、轻量锁
当多个线程竞争锁时,偏向锁就会撤销,偏向锁撤销之后会升级为轻量级锁。这时候的多线程竞争锁是指 多个线程交替执行,多个线程可能不是顺序执行,会自旋一个线程上下文的时间,再去获取锁,也会是轻量级锁。
多个线程交替执行有以下几种方式:
I、第一个线程结束,第二个线程执行 没有竞争——偏向锁膨胀为轻量级锁
II、第一个线程 同步结束,线程未结束,第二个线程执行 ——没有竞争——偏向锁膨胀为轻量级锁。
8、重量锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
9、偏向锁、轻量级锁、重量级锁性能对比
我们测试一下调用同步方法10亿来计算10亿的i++,对比偏向锁和轻量级锁、重量级锁的性能。
public class A {
public synchronized void parse(){
JOLExample6.countDownLatch.countDown();
}
}
首先设置关闭偏向锁延迟的情况下运行线面代码测试偏向锁性能。(添加 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 来禁用偏向锁延迟)。再开启偏向锁延迟运行下面代码测试轻量锁性能。
public class JOLExample4 {
public static void main(String[] args) throws Exception {
A a = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<1000000000L;i++){
a.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
out.println(ClassLayout.parseInstance(a).toPrintable());
}
测试重量级锁性能:
public class JOLExample6 {
static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
public static void main(String[] args) throws Exception {
final A a = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<2;i++){
new Thread(){
@Override
public void run() {
while (countDownLatch.getCount() > 0) {
a.parse();
}
}
}.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}}
运行结果如下所示:
偏向锁 | 轻量锁 | 重量锁 |
---|---|---|
3553ms | 29075ms | 61359ms |
由表可知,在性能上面,偏向锁>轻量锁>重量锁。
10、锁的批量重偏向和批量撤销重偏向
前面提到了JVM关于锁的几个默认参数中有BiasedLockingBulkRdbiasThreshold=20: 偏向锁默认批量重偏向的阈值。以及
BiasedLockingBulkRevokeThreshold=40:偏向锁默认批量撤销的阈值。我们可以通过代码进行验证。
public class JOLExample12 {
static List<A> list = new ArrayList<A>();
static List<A> list2 = new ArrayList<A>();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread() {
public void run() {
for (int i=0;i<100;i++){
A a = new A();
synchronized (a){
list.add(a);
list2.add(a);
}
}
}
};
t1.start();
t1.join();
out.println("befre t2");
out.println("list.get(1) 是偏向锁:"+ClassLayout.parseInstance(list.get(1)).toPrintable());
out.println("list.get(50) 是偏向锁:"+ClassLayout.parseInstance(list.get(50)).toPrintable());
Thread t2 = new Thread() {
public void run() {
for (int i = 0; i < list.size(); i++){
A a = list.get(i);
synchronized (a){
//i=10 未达到批量重偏向的阈值20,还是轻量级锁
if (i==10){
out.println("轻量级锁 t2 ing ------------"+i );
out.println(ClassLayout.parseInstance(a).toPrintable());
}
//i=19 达到批量重偏向的阈值20,撤销为偏向锁
if (i==19){
out.println("偏向锁 t2 ing ------------"+i );
out.println(ClassLayout.parseInstance(a).toPrintable());
//到此为止,累计重偏向20次
System.out.println("到此为止,a对象累计重偏向20次");
}
//i=24 超过批量重偏向的阈值20,都为偏向锁
if (i==24){
out.println("偏向锁 t2 ing ------------"+i );
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
}
};
t2.start();
t2.join();
//添加多余线程,防止线程ID复用
new Thread() {
public void run() {
System.out.println("tmp---------------------------------------------");
}
}.start();
Thread t3 = new Thread() {
public void run() {
for (int i = 20; i < list.size(); i++) {
A a=list.get(i);
if (i==20){
out.println("t3 ing 未加锁状态下,预期是t2的偏向锁 ------------" +i);
out.println(ClassLayout.parseInstance(a).toPrintable());
}
synchronized (a){
//i=20 没有达到批量撤销偏向锁的阈值40 是轻量锁
if (i==20){
out.println("轻量级锁 t3 ing ------------" +i);
out.println(ClassLayout.parseInstance(a).toPrintable());
}
//i=40 加上前面的20次,累计达到批量撤销偏向锁的阈值40 撤销t2偏向锁 升级为轻量级锁
if (i==40){
out.println("撤销t2偏向锁 升级为轻量级锁 t3 ing ------------" +i);
out.println(ClassLayout.parseInstance(a).toPrintable());
}
//i=25 累计达到批量撤销偏向锁的阈值40 撤销t2偏向锁 升级为轻量级锁
if (i==50){
out.println("轻量级锁 t3 ing ------------" +i);
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
}
};
t3.start();
}
}
运行结果如下:
befre t2
list.get(1) 是偏向锁:com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 30 22 28 (00000101 00110000 00100010 00101000) (673329157)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
list.get(50) 是偏向锁:com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 30 22 28 (00000101 00110000 00100010 00101000) (673329157)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
轻量级锁 t2 ing ------------10
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f0 95 29 (01100000 11110000 10010101 00101001) (697692256)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁 t2 ing ------------19
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 81 10 29 (00000101 10000001 00010000 00101001) (688947461)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
到此为止,a对象累计重偏向20次
偏向锁 t2 ing ------------24
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 81 10 29 (00000101 10000001 00010000 00101001) (688947461)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
tmp---------------------------------------------
t3 ing 未加锁状态下,预期是t2的偏向锁 ------------20
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 81 10 29 (00000101 10000001 00010000 00101001) (688947461)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
轻量级锁 t3 ing ------------20
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 20 f2 b5 29 (00100000 11110010 10110101 00101001) (699789856)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
撤销t2偏向锁 升级为轻量级锁 t3 ing ------------40
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 20 f2 b5 29 (00100000 11110010 10110101 00101001) (699789856)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
轻量级锁 t3 ing ------------50
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 20 f2 b5 29 (00100000 11110010 10110101 00101001) (699789856)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 c2 00 f8 (01100001 11000010 00000000 11111000) (-134167967)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t1给对象a加锁,list中的a对象全部是t1持有的偏向锁100个。t2执行加锁的过程中拿到t1持有的偏向锁,会撤销t1偏向锁,升级为t2轻量级锁,t2经过20次锁撤销,将后面的80个t1持有的偏向锁全部转换为为t2持有的偏向锁(20前面的锁不会变,还是t2持有的轻量锁,20次以后 就只会进行if判断,到100次)。
这时候如果t3来加锁,假如从20开始,将t2持有的偏向锁进行撤销,升级为t3轻量锁,当达到40的阈值后(也就是类对的对象累计撤销40次),根据jvm的设置,不会将t3的轻量锁重偏向为偏向锁,而是将40以后的t2的偏向锁撤销,升级为t3持有的轻量锁。
总结:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向锁撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就会认为该class 的偏向锁有问题,因此会进行批量重偏向。每个class 对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值设置为创建该对象时,class中的epoch值,每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程,找到该class所有正处于加锁状态的偏向锁,将其epoch值改为新值,下次获取锁时,当发现当前对象的epoch值和class的epoch值不相等,那就算当前已经偏向了其他线程,也不会进行撤销操作,而是直接通过CAS操作箱其Mark Word的Thread id改为当前线程id。
11、锁的膨胀
锁的膨胀过程就是 偏向锁 到轻量锁 到 重量锁的过程。下面盗用一张图来说明具体流程。