对象头包含两部分:运行时元数据(Mark Word)和类型指针 (Klass Word)
Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。
类型指针
类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
synchronized修饰代码块
当我们用synchronized修饰代码块时字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。
代码举例:
/**
* 当我们使用synchronized关键字来修饰代码块时,
* 字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。
* monitorenter跟monitorexit 是一对多的关系
*
* 当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令,
* 线程将会释放Monitor对象
*/
@Slf4j
public class Test {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。
/**
* 对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。
*
* JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC——SYNCHRONIZED标志
* 如果有,那么执行线程将会先持有方法所在对象的Monitor对象,然后再去执行方法体;在改方法执行期间,其他任何线程均无法在获取到这个
* monitor,当线程执行完该方法后,它会释放掉这个Monitor对象。
*/
public class MyTest2 {
public synchronized void method() {
System.out.println("hello world");
}
}
/*
0 getstatic #2
3 ldc #3
5 invokevirtual #4
8 return
*/
此时就没有通过monitorebter和moniterexit 来获取锁而是通过ACC_SYNCHRONIZED标识符来尝试获取锁
synchronized修饰静态方法
/**
* 当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC
* 其次锁的是 类锁
*/
public class MyTest3 {
/**
* static静态方法不存在this局部变量
* 原因直接类名.就能调用
*/
public static synchronized void method() {
System.out.println("hello world!");
}
}
/*
0 getstatic #2
3 ldc #3
5 invokevirtual #4
8 return
*/
从JDK1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁,轻量级锁及重量级锁等,从而减少锁的竞争所带来的的用户态(如程序执行业务代码在用户端)与内核态(Monitor是依赖于底层操作系统 此时阻塞就是内核执行)之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;对应锁的访问与改变,实际上都与Java对象头息息相关。
对于synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标志位与是否偏向锁标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁开始的, 随着锁的竞争不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。
对于锁的演化来说,它会经历如下阶段:
无锁 -> 偏向锁 ->轻量级锁 ->重量级锁
无锁:当前对象没有线程访问
偏向锁
只针对单个线程同步: 针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象 就会在其Mark word中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个对象锁的synchronized方法时,它会检查这个对象 的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进行管程(Monitor)了,而是直接进入到该方法体中。
如果是另一个线程访问这个synchronized方法,那么实际情况会如何了?
偏向锁会被取消掉
轻量级锁
多个线程同步,若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁, 而第二个线程在争抢时,会发现改对象的对象头中的Mark Word已经是偏向锁了,但里面存储的线程ID并不是自己(第一个线程),那么会进行CAS(Compare and Swap),从而获取到锁这里面存在两种情况:
自旋锁
若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)
自旋最大的一个特点就是避免了线程冲用户态进入带内核态。
重量级锁
线程最终从用户态进入到内核态.
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞 争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
创建锁记录 (Lock Record) 对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对 象的 Mark Word
让锁记录中Object reference指向锁对象,并尝试用cas替换Object的 Mark Word, 将 Mark Word的值存入 锁记录
如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
如果cas失败,有两种情况
如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程 。
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record作为重入的计数。
当退出 synchronized 代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示 重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为null, 这时使用cas将 Mark Word的值恢复给对象 头。
成功则解锁成功。
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级 锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
@Slf4j
public class Test {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
当 Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
这时Thread-1 加轻量级锁失败,进入锁膨胀流程
即为Object对象申请Monitor锁,让 Object指向重量级锁地址
然后自己进入Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用cas将 Mark Word 的值恢复给对象头,失败.这时会进入重量级解锁 流程,即按照Monitor 地址找到Monitor对象,设置Owner 为 null, 唤醒 EntryList 中 BLOCKED线程
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了 同步块,释放了锁),这时当前线程就可以避免阻塞。
不会马上让当前线程进入等待状态,而是进行自旋操作,如果自旋成功,就不需要进行上下文切换,优化了性能
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word 头,之后 发现这个线程ID是自己的就表示没有竞争,不用重新 CAS。 以后只要不发生竞争,这个对象就归该线程所有
只针对单个线程同步: 针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象 就会在其Mark word中的将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个对象锁的synchronized方法时,它会检查这个对象 的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进行管程(Monitor)了,而是直接进入到该方法体中。
例如:
public class Test {
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步块A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块B
m3();
}
}
public static void m3() {
synchronized (obj) {
// 同步块C
}
}
}
其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
撤销偏向锁 - 调用 wait/notify (只有重量级锁才支持这两个方法)
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重 置对象的 Thread ID
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏 向至加锁线程
当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有 对象都会变为不可偏向的,新建的对象也是不可偏向的
编译器对于锁的优化措施: (锁消除和锁粗化都是针对运行期的 且针对代码块)
JIT编译器(Just In Time编译器)可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,来通过该项技术判别程序中所使用的锁对象是否只被 一个线程所使用,而没有散布到其他线程当中;如果情况就是这样的话,那么JIT编辑器在编译这个同步代码时就不会生成synchronized关键字所标识 的锁的申请与释放机器码,从而消除了锁的使用流程。
/**
* 锁消除技术
*/
public class MyTest4 {
public void method() {
/*
属于栈被每个线程所独有object 不共享 此时synchronized 被消除是不执行的
但字节码还是会生成monitorenter和monitorexit
*/
Object object = new Object();
synchronized (object) {
System.out.println("hello world");
}
}
}
JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个对象的锁(monitor),那么它就会把这几个synchronized块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁各一次,就可以执行完全部的同步代码块,从而提升了性能。
/**
* 锁粗化
*/
public class MyTest5 {
Object o = new Object();
public void method() {
synchronized (o) {
System.out.println("hello world");
}
synchronized (o) {
System.out.println("hello ");
}
synchronized (o) {
System.out.println(" world");
}
}
}
死锁案例
/*
* jps -l
* jstack 进程id
*/
public class MyTest6 {
//生成两个不同的对象每个对象都有一个monitor
private Object lock1 = new Object();
private Object lock2 = new Object();
public void myMethod1() {
synchronized (lock1) {//这里锁的是成员变量对象1
synchronized (lock2) {//这里锁的是成员变量对象2
System.out.println("myMethod1 invoked");
}
}
}
public void myMethod2() {
synchronized (lock2) {
synchronized (lock1) {
System.out.println("myMethod2 invoked");
}
}
}
public static void main(String[] args) {
MyTest6 myTest6 = new MyTest6();
Runnable run1 = () -> {
while (true) {
myTest6.myMethod1();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread myThread1 = new Thread(run1, "myThread1");
Runnable run2 = () -> {
while (true) {
myTest6.myMethod2();
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread myThread2 = new Thread(run2, "myThread2");
myThread1.start();
myThread2.start();
}
}
jstack
D:\workspace\jvm\jvm>jps -l
7536 sun.tools.jps.Jps
10708 org.jetbrains.jps.cmdline.Launcher
10472 org/netbeans/Main
7976
8076 org.jetbrains.jps.cmdline.Launcher
8316 com.example.demo.com.concurrecy.concurrency3.MyTest6
9468 org.jetbrains.jps.cmdline.Launcher
D:\workspace\jvm\jvm>jstack 8316
Found one Java-level deadlock:
=============================
"myThread2":
waiting to lock monitor 0x000000001c789108 (object 0x000000076ba72f40, a java.lang.Object),
which is held by "myThread1"
"myThread1":
waiting to lock monitor 0x000000001c789058 (object 0x000000076ba72f50, a java.lang.Object),
which is held by "myThread2"
Java stack information for the threads listed above:
===================================================
"myThread2":
at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod2(MyTest6.java:29)
- waiting to lock <0x000000076ba72f40> (a java.lang.Object)
- locked <0x000000076ba72f50> (a java.lang.Object)
at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$1(MyTest6.java:52)
at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$2/708049632.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"myThread1":
at com.example.demo.com.concurrecy.concurrency3.MyTest6.myMethod1(MyTest6.java:21)
- waiting to lock <0x000000076ba72f50> (a java.lang.Object)
- locked <0x000000076ba72f40> (a java.lang.Object)
at com.example.demo.com.concurrecy.concurrency3.MyTest6.lambda$main$0(MyTest6.java:39)
at com.example.demo.com.concurrecy.concurrency3.MyTest6$$Lambda$1/2085857771.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.