在上一篇文章中并发编程之AQS探秘中,我们介绍了AQS的运用及实现原理,同时简单的展望了一下JUC包的大致结构及套路。其实提到并发编程就不得不提到一个词
锁
,而提到锁又始终绕不过去Java自带的锁synchronized
。下面我们就来探究下synchronized的前世今生。本文主要包括以下几部分:
- 前言
- 锁
2.1 锁分类
2.1.1 按照实现划分
2.1.2 按照特性划分
2.2 五花八门的锁
2.2.1 悲观锁、乐观锁
2.2.2 自旋锁、自适应自旋锁
2.2.3 无锁、偏向锁、轻量级锁、重量级锁
2.2.4 公平锁、非公平锁
2.2.5 可重入锁、不可重入锁
2.2.6 共享锁、排它锁- synchronized 基础
3.1 实例方法
3.2 静态方法
3.3 代码块- synchronized 原理
4.1 Monitor
4.2 锁方法
4.3 锁代码块- 锁信息的存储
5.1 对象的内存布局
5.2 锁信息的存储之MarkWord- 锁优化
6.1 锁升级的过程
6.2 无锁
6.3 偏向锁
6.4 轻量级锁
6.5 重量级锁- 总结
1. 前言
谈到并发编程,就不得不提并发编程的一哥锁
,而提到锁就绕不开synchronized
。而想要搞清楚synchronized的原理,我们必须要搞清楚锁的数据结构,JDK1.6后带来的锁优化
,同时既然提到了锁,就很有必要了解锁的分类
,故下面我们会按照锁分类、synchronized用法、synchronized原理、锁信息的存储、锁优化这个顺序来介绍。
老套路,先思考几个问题。俗话说的好,不带着问题学习的Java开发工程师,不是一个好的UI
问题1:java中都有哪些锁?各自的使用场景又是什么?
问题2:synchronized如何使用?原理是什么?
问题3:如何计算对象的大小?
问题4:虚拟机栈是如何构成的?
问题5:锁升级的过程是什么?
问题6:什么是偏向锁?什么是轻量级锁?什么是重量级锁?能否降级?
问题7:JDK对锁做了哪些优化?
下面带着这些疑问,我们继续下面的内容。
2. 锁
多线程环境下,解决资源竞争时的数据安全性问题,我们一般有两种思路。
-
资源的复制,即每个线程复制一份资源,相当于每个线程持有自己的资源。典型的实现比如ThreadLocal
。 -
将并发的执行变成串行的执行,即不同线程之间的执行顺序具有互斥性。典型的方式加锁,synchronized、Lock等
。
2.1 锁分类
Java中的锁按照不同的维度可以做不同的划分。
2.1.1 按照实现划分
语言级别(关键字)的锁(自带的synchronized)
语义级别的锁(基于特殊的数据结构+volatile+CAS实现的锁。ReentrantLock、ReadWriteLock等)
2.1.2 按照特性划分
除了按照实现粗略划分,还可以按照锁是否有某种特性来划分。这种情况下,可以分为如下锁:
悲观锁、乐观锁、自旋锁、自适应自旋锁、无锁、偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、可重入锁、不可重入锁、共享锁、排它锁(互斥锁)
共14种。具体区分详见如下思维导图:
有一点需要特别说明的上述14种锁,只是说按照锁的特性可以拆分为14种不同特性的锁。但是并不是有14种不同种类的实现。
拿synchronized来说,它即是悲观锁,又是非公平锁,又是可重入锁,同时又包含了四种锁的状态:无锁、偏向锁、轻量级锁、重量级锁。
一个synchronized包含了七种特性,但却仅仅有一种实现。所以锁的种类和实现的种类是不对等的。
2.2 五花八门的锁
2.2.1 悲观锁、乐观锁
悲观锁和乐观锁并不是java种特有的实现,而是一个广义的概念,具体来说:
悲观锁认为在多线程竞争的情况下,使用数据时一定会有别的线程来修改数据,因此要先加锁。
乐观锁则认为在多线程竞争的情况下,使用数据时不会有别的线程来修改数据,只会在提交数据的时候才会去比较数据在这期间是否被修改。如果未被修改则成功提交,如果被修改则按照既定的策略操作(报错和或重试)。
悲观锁和乐观锁在不同的产品种有不同的实现。比如:
- 悲观锁:
Java种的synchronized和Lock。数据库种的select for update。利用redis或者zk实现的分布式锁
。这些都是悲观锁。 - 乐观锁:
Java的CAS、各种原子操作类。数据库加入版本号字段。
同时由于悲观锁和乐观锁的特性,它们分别有不同的应用场景:
- 悲观锁:
适合写多都少的场景,一上来就加锁可以最大的保证数据安全性,但是会带来性能的损失。
- 乐观锁:
适合写少读多的场景,最后提交的时候才进行比较,可以最大的保证性能。
值得注意的是:并不是说使用乐观锁如CAS一定性能就比悲观锁高,要区分具体场景。比如在线程竞争度非常高的时候,使用CAS反而不如使用synchronized。因为竞争度高的情况下CPU往往浪费在无法成功的自旋上,此时还不如让线程阻塞挂起,等待获得锁后继续运行来的高效。
2.2.2 自旋锁、自适应自旋锁
说自旋锁、自适应自旋锁时,我们先要明白一个前提条件cpu的执行时间要比线程切换的速度快得多
,一个操作CPU执行时间可能仅仅需要1ns,而线程上下文切换可能需要10000ns。
阻塞和唤醒线程需要涉及到线程上下文的切换,如果阻塞的代码块的执行时间远比线程切换的时间短的情况下,再为了一段执行时间很短的代码,去做耗时更长的线程上下文切换,这样显然时不合理的。那么有没有办法让当前线程等一等
呢。
-
线程不去挂起,而是进入自旋状态。如果自旋结束后,其它线程释放了锁,则线程可以成功获得锁,而避免了挂起和恢复线程时带来的消耗。如果未获取到,则进行循环尝试。这个就是自旋锁的定义
。这里有一个问题,如果一直获取获取不到锁,就一直自旋吗,显然是不合适的,一直自旋会造成CPU的浪费,因此需要设置一个自旋最大次数。 -
JDK 1.6后对自旋锁进行了优化,可以根据情况,按照一定的算法自动调整自旋次数。这个就叫做自适应自旋锁。
具体调整的规则为,如果自旋等待刚成功过且持有锁的线程正在运行,则允许更长的自旋时间。如果自旋等待很少会成功,那么可能下次就放弃自旋直接阻塞,避免CPU的浪费。
同时自旋锁和自适应自旋锁都是利用CAS实现的
2.2.3 无锁、偏向锁、轻量级锁、重量级锁
这里的无锁、偏向锁、轻量级锁、重量级锁都是指synchronized内部锁升级的过程。
随着竞争的愈发激烈,锁会经历由无锁->偏向锁-> 轻量级锁->重量级锁的升级过程。
-
锁升级的过程是不可逆的,也就是锁不能降级。
要讲清楚锁升级就不得不提锁的存储,后面的部分我们会详细介绍。
2.2.4 公平锁、非公平锁
公平和非公平指的是想要获取锁的线程需不需要排队。定义如下:
公平锁指的是,申请获取锁的线程按照申请锁的顺序获取锁,未获得锁的线程入队排队,队列头部线程比尾部的线程先获得锁。遵循FIFO。
非公平锁指的是,申请获得锁的线程直接尝试获取锁,如果此时刚好获得锁,则能在不阻塞的情况下获得锁。如果此时未获取到锁,则插入队列尾部排队(队列遵循FIFO)。
公平锁和非公平锁由于不同的特性,有不同的特点
公平锁由于遵循FIFO,可以保证线程不会饿死。但是性能不如非公平锁,线程切换的开销也比非公平锁大。
非公平锁存在后获取锁的线程先得到锁,故可能存在线程饿死。但是性能要比公平锁高。
Lock fairLock=new ReentrantLock();
Lock nonFairLock=new ReentrantLock(false);
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2.5 可重入锁、不可重入锁
-
可重入锁指的是同一个线程在外层方法获得了锁,再调用锁定同一个对象或者class的内层方法的时候,可以无需再次获得锁。
synchronized、ReentrantLock 、ReentrantReadWriteLock都是可重入的。 非可重入锁正好跟可重入锁相反。
public class SynchronizedTest {
public synchronized void methodA(){
System.out.println("do something in methodA...");
methodB();
}
public synchronized void methodB(){
System.out.println("do something in methodB...");
}
public static void main(String[] args) {
SynchronizedTest test=new SynchronizedTest();
test.methodA();
}
}
毫无疑问,上面的代码是可以正常运行的。
do something in methodA...
do something in methodB...
正常运行的前提正是synchonized
,试想如果synchronized
不可重入,那么在调用methodA里调用methodB时,因为methodA未释放持有的锁,同时又等待methodB持有的锁,因为是同一个锁,则会造成死锁。
这也就从侧面说明了可重入锁的优势:可以一定程度上避免死锁
2.2.6 共享锁、排它锁
排它锁又名互斥锁或者独享锁。指的是同一时刻只有一把线程能获得锁。如synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock。
-
共享锁顾名思义就是同一时刻可以被多个线程持有的锁。比如ReentrantReadWriteLock.ReadLock。
关于Lock相关实现会在后续文章中刨析,这里就不过多解释了。
3. synchronized 基础
上面我们大概介绍了下锁的分类,下面进入本文的核心内容synchronized,介绍原理之前,我们要先了解其用法。
3.1 实例方法
public class SynchronizedTest {
public synchronized void methodA(){
System.out.println(Thread.currentThread().getName()+" 持有锁 @ "+ LocalDateTime.now().toString());
System.out.println("do something in methodA...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 释放锁 @ "+ LocalDateTime.now().toString());
}
public static void main(String[] args) {
SynchronizedTest test=new SynchronizedTest();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 尝试获取锁 @ "+ LocalDateTime.now().toString());
test.methodA();
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 尝试获取锁 @ "+ LocalDateTime.now().toString());
test.methodA();
},"t2").start();
}
}
运行结果如下
t2 尝试获取锁 @ 2020-12-03T16:56:53.600
t1 尝试获取锁 @ 2020-12-03T16:56:53.600
t2 持有锁 @ 2020-12-03T16:56:53.600
do something in methodA...
t2 释放锁 @ 2020-12-03T16:56:56.602
t1 持有锁 @ 2020-12-03T16:56:56.602
do something in methodA...
t1 释放锁 @ 2020-12-03T16:56:59.602
可以看到,后尝试获得锁的线程要等待先获得锁的线程释放锁,才能进入方法。
synchronized 此时锁的是这个实例,即 test
3.2 静态方法
public class SynchronizedStaticTest {
public static synchronized void methodA(){
System.out.println(Thread.currentThread().getName()+" 持有锁 @ "+ LocalDateTime.now().toString());
System.out.println("do something in methodA...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 释放锁 @ "+ LocalDateTime.now().toString());
}
public static void main(String[] args) {
SynchronizedTest test1=new SynchronizedTest();
SynchronizedTest test2=new SynchronizedTest();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 尝试获取锁 @ "+ LocalDateTime.now().toString());
test1.methodA();
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 尝试获取锁 @ "+ LocalDateTime.now().toString());
test2.methodA();
},"t2").start();
}
}
输出结果
t2 尝试获取锁 @ 2020-12-03T17:04:31.638
t1 尝试获取锁 @ 2020-12-03T17:04:31.638
t2 持有锁 @ 2020-12-03T17:04:31.639
do something in methodA...
t1 持有锁 @ 2020-12-03T17:04:31.639
do something in methodA...
t1 释放锁 @ 2020-12-03T17:04:34.640
t2 释放锁 @ 2020-12-03T17:04:34.640
我们把锁实例对象的代码进行了部分改变,方法用static修饰,同时线程调用的时候也传入的是不同的对象,发现还是两个线程还是要竞争同一把锁。
synchronized此时锁的是整个class,即SynchronizedStaticTest.class
3.3 代码块
public class SynchronizedCodeBlockTest {
private static final Object LOCK = new Object();
public void methodA() {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁 @ " + LocalDateTime.now().toString());
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " 持有锁 @ " + LocalDateTime.now().toString());
System.out.println("do something in methodA...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedCodeBlockTest synchronizedCodeBlockTest=new SynchronizedCodeBlockTest();
new Thread(synchronizedCodeBlockTest::methodA,"t1").start();
new Thread(synchronizedCodeBlockTest::methodA,"t2").start();
}
}
输出结果
t1 尝试获取锁 @ 2020-12-03T17:20:42.488
t2 尝试获取锁 @ 2020-12-03T17:20:42.488
t1 持有锁 @ 2020-12-03T17:20:42.489
do something in methodA...
t2 持有锁 @ 2020-12-03T17:20:45.489
do something in methodA...
可以看到,synchronized此时锁的是特定对象,作用范围为整个代码块
4. synchronized原理
要搞清楚synchronized原理,需要准备的知识点比较多:Mointor对象、字节码指令集、MarkWorkd、锁升级。这里我们先看下Monitor、然后再再看下对应的字节码。
4.1 Monitor
Mointor的定义
每个Java对象都自带一个锁,这个锁就是Monitor
。每个对象都有一个Monitor与之相关联,这个Monitor可以是在对象创建或者在我们试图用synchronized获取锁的时候创建
,也可以在对象销毁的时候被一起销毁
。我们上锁解锁的过程其实是操作monitor的过程。`
Mointor的数据结构
Mointor是由java虚拟机实现的,对应的源码文件为hotspot-f06c7b654d63\src\share\vm\runtime\objectMonitor.cpp
和 hotspot-f06c7b654d63\src\share\vm\runtime\objectMonitor.hpp
。我们截取objectMonitor.hpp一段定义文件
// JVM/DI GetMonitorInfo() needs this
ObjectWaiter* first_waiter() { return _WaitSet; }
ObjectWaiter* next_waiter(ObjectWaiter* o) { return o->_next; }
Thread* thread_of_waiter(ObjectWaiter* o) { return o->_thread; }
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
_count:计数器,获取锁+1,释放锁-1。
_owner:持有当前Monitor的线程引用。
_WaitSet:等待队列,调用wait方法时加入。
_EntryList:等待获取锁的队列。
Monitor的工作流程
- Monitor包含两个队列,
_EntryList和_WaitSet
。每个尝试获取Monitor的线程都会被封装为ObjectWaiter对象,并进入_EntryList
。 - 多线程在执行到同步代码块的时候,会尝试获取Monitor,但是
同一时刻一个Monitor对象只能被一个线程持有。一旦该线程持有该Monitor,则Mointor则处于锁定状态
。成功获取monitor的线程由_EntryList出队进入_owner,且count+1
。如果在执行同步代码块中发生了重入,则count继续+1
。 - 调用wait时,会释放Mointor,并
_owner设置为null,count-1,同时进入_WaitSet。
- 当调用notify()/notifyAll()的时候,
_WaitSet中的线程会被唤醒一个或者多个,这些线程需要重新获取monitor,才能从wait()状态返回,执行后面的代码。这个过程经历了由_WaitSet到_owner
。 - 执行完同步代码块后,
释放monitor,并复位其变量值
,
之前我们在将AQS的时候,提出过这么一个问题为什么wait()和notify()/notifyAll()只能用在同步块中?
原因是wait()/notify()/notifyAll()是monitor的方法,而要持有monitor的方式就是使用synchronized
。
证据如下:
bool try_enter (TRAPS) ;
void enter(TRAPS);
void exit(bool not_suspended, TRAPS);
void wait(jlong millis, bool interruptable, TRAPS);
void notify(TRAPS);
void notifyAll(TRAPS);
上面的代码摘自hotspot-f06c7b654d63\src\share\vm\runtime\objectMonitor.hpp
4.2 锁方法
反编译一下3.1字节码文件
javap -p -verbose SynchronizedTest
public synchronized void methodA();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED//权限为public,且为同步方法
Code:
stack=3, locals=2, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."":()V
10: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
13: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #8 // String 持有锁 @
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokestatic #9 // Method java/time/LocalDateTime.now:()Ljava/time/LocalDateTime;
27: invokevirtual #10 // Method java/time/LocalDateTime.toString:()Ljava/lang/String;
30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
可以看到,同步方法只是多了一个标识ACC_SYNCHRONIZED
。JVM通过这个标志标识一个方法是同步方法。如果一个方法被设置了该标识,则执行方法前需要先获取Monitor,只有当获取到Monitor后才能执行方法里的代码。当方法执行完,无论是正常执行完还是抛异常,都会释放获得的Monitor
。JVM正是通过ACC_SYNCHRONIZED标识来实现同步方法的。
4.3 锁代码块
反编译下 3.3 中的代码
public void methodA();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."":()V
10: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
13: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #8 // String 尝试获取锁 @
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokestatic #9 // Method java/time/LocalDateTime.now:()Ljava/time/LocalDateTime;
27: invokevirtual #10 // Method java/time/LocalDateTime.toString:()Ljava/lang/String;
30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: getstatic #13 // Field LOCK:Ljava/lang/Object;
42: dup
43: astore_1
44: monitorenter //注意这里
45: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
48: new #3 // class java/lang/StringBuilder
51: dup
52: invokespecial #4 // Method java/lang/StringBuilder."":()V
55: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
58: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
61: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
64: ldc #14 // String 持有锁 @
66: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
69: invokestatic #9 // Method java/time/LocalDateTime.now:()Ljava/time/LocalDateTime;
72: invokevirtual #10 // Method java/time/LocalDateTime.toString:()Ljava/lang/String;
75: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
78: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
81: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
84: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
87: ldc #15 // String do something in methodA...
89: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
92: getstatic #16 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
95: ldc2_w #17 // long 3l
98: invokevirtual #19 // Method java/util/concurrent/TimeUnit.sleep:(J)V
101: goto 109
104: astore_2
105: aload_2
106: invokevirtual #21 // Method java/lang/InterruptedException.printStackTrace:()V
109: aload_1
110: monitorexit //注意这里
111: goto 119
114: astore_3
115: aload_1
116: monitorexit //注意这里![20170620931260_zdrwIu.gif](https://upload-images.jianshu.io/upload_images/10505542-71bb6ca2fc00aa83.gif?imageMogr2/auto-orient/strip)
117: aload_3
118: athrow
119: return
Exception table:
from to target type
92 101 104 Class java/lang/InterruptedException
45 111 114 any
114 117 114 any
注意 行号为44和110的字节码,我们发现了两个关键的字节码指令monitorenter、monitorexit
-
monitorenter标记的是同步代码块开始的位置。用来标记这里要开始获取被锁定对象的monitor
,如果此时该对象的monitor没有被锁定(_owner=null && count==0)则有机会持有monitor对象,持有monitor对象后将_owner指向自己,并将count+1。 -
monitorexit标记的是同步代码结束的位置。用来标记这里要释放被synchronized锁定对象的monitor
,释放monitor对象时,会将monitor的属性复位(set count=0,_owner=null)。这样其它线程就有机会获取monitor。
问:一条monitorenter必然对应一条monitorexit,但是我们在116行字节码里发现了另一条monitorexit,为什么多出一条monitorexit呢?
,
答:为了保证程序无论是在正常退出和异常退出的情况下都能释放monitor,编译器会自动生成一个全局的异常处理器,这个全局异常处理器用来处理所有异常,多出来的一条monitorexit指令就是全局异常处理器用来在异常情况下释放monitor的字节码。
类似try/finally
5. 锁信息的存储
上面我们分析了synchronized的原理,如果你以为这样就结束了,那是不可能的。
monitor的底层是基于操作系统层面的Mutex Lock,这种锁会导致线程的阻塞和上下文切换
。频繁的阻塞唤醒所导致的上下文切换会带来严重的性能损耗,所以JDK 1.6之前是synchronized效率是比较低的,也就是我们常说的重量级锁。JDK 1.6之后对synchronized进行了优化,加入了偏向锁和轻量级锁
。所以存在锁的状态转换的过程。但是要搞清楚锁的状态转换,我们要先了解锁的存储结构。
5.1 对象的内存布局
- JVM体系结构
先来张大家都特别熟悉的图
就拿3.3中的代码
private static final Object LOCK = new Object();
那这里的new Object()究竟在内存的哪块区域呢,答案是堆
。堆也是整个JVM内存中最大的一块区域。
- 对象内存布局
那么其在堆中究竟是以什么样的结构存在呢?这就是我们即将要讲到的重点,对象的内存布局。
对象的内存布局主要分为几部分:MarkWord、klass pointer、array size、Instance Data、Padding
。
- Object Header
MarkWord、klass pointer、array size 我们称之为对象头(Object Header)
。
结构体 | 描述 | 32位VM | 64位VM未开启指针压缩 | 64位VM开启指针压缩 |
---|---|---|---|---|
MarkWord | 一系列标记信息,如hashcode、持有线程id、是否偏向锁、GC年龄等 | 4字节 | 8字节 | 8字节 |
Klass Pointer | 指向元数据的地址。用来标识对象的类型。 | 4byte | 8byte | 4byte |
Array Length | 用来标识数组的长度(当前对象为数组的时候才会有存在) | 4byte | 8byte | 4byte |
4.实例数据与对齐填充
Instance Data 即实例数据,其占用的大小是根据实际情况来看的。
Padding 即对齐填充,JVM规定java 对象在内存里按照8字节对齐。
import org.openjdk.jol.info.ClassLayout;
public class Animal {
private Cat cat=new Cat();
private int num=10;
private class Cat{
}
public static void main(String[] args) {
Animal animal=new Animal();
//8+4+4+4+4=24
System.out.println(ClassLayout.parseInstance(animal).toPrintable());
}
}
这里我们仅仅创建了一个Animal对象,那么这个Object究竟占用了多大内存呢?
64位虚拟机,JDK 版本1.8 且开启指针压缩(JDK 1.8默认开启,可以使用-XX:-UseCompressedOops关闭)
按照上面的说法,animal所占用的内存应该为:8byte MarkWord+4byte Klass Pointer+(4byte cat指针+4byte num=8byte Instance Data)=20byte,20不能被8整除,因此需要补4byte,最终animal所占内存为24byte
。执行我们上面的代码,看看结果是否如我们所料呢?
syn.Animal object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int Animal.num 10
16 4 syn.Animal.Cat Animal.cat (object)
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
果然animal 占用了24byte的大小
。
同时从输出的结果里,能很明显的看出我们上面提到的对象的内存布局的结构,依次为:MarkWord、Klass Poniter、Instance Data、Padding。
5.2 锁信息的存储之MarkWord
上面我们介绍了对象的内存布局,大致了解了对象在堆中以一个怎样的数据结构存在。下面就进入到与synchronized锁密切相关的部分了----MarkWord。
The markOop describes the header of an object.
Note that the mark is not a real oop but just a word.
It is placed in the oop hierarchy for historical reasons.
Bit-format of an object header (most significant first, big endian layout below):
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
以上注释摘自hotspot源码hotspot-f06c7b654d63\src\share\vm\oops\markOop.hpp
。可以清楚的看到markWork分为32位VM、64位VM未开启指针压缩、64位虚拟机开启指针压缩三种情况。
-
32位VM Object Header
-
64位VM 未开启指针压缩 Object Header
-
64位VM 开启指针压缩 Object Header
名词解释
名词 | 含义 | 说明 |
---|---|---|
identity_hashcode | 对象的原始hashcode |
非重写的hashcode,可以使用System.identityHashCode(o)获得 |
age | 对象的分代年龄 |
保存对象在survivor区熬过的GC次数 |
biased_lock | 是否偏向锁 |
0:否 1:是 |
lock | 锁标记位 |
不同状态锁标记位不同 |
thread | 线程id |
持有偏向锁的线程id |
epoch | 偏向时间戳 |
计算偏向锁是否有效 |
ptr_to_lock_record | 指向栈中lock_record的指针 |
轻量级锁在获取的时候会在栈帧中创建Lock Record,并将Mark Word 复制到Lock Record中 |
ptr_to_heavyweight_monitor | 指向重量级锁的指针 |
Mutex Lock |
不同锁状态下的biased_lock与lock
锁状态 | 是否偏向锁 | 锁标记 | 说明 |
---|---|---|---|
无锁 | 0 | 01 | 无 |
偏向锁 | 1 | 01 | 无 |
轻量级锁 | null | 00 | 无 |
重量级锁 | null | 10 | 无 |
GC | null | 11 | 对象可以被回收 |
问:为什么要开启指针压缩?
答:为了节省空间,提高空间利用率(Object Header 由 128bits->96bits)
。
问:为什么要对齐填充?
答:为了提高寻址效率,让字段不跨缓存行
问:指针压缩后最大寻址是不是只有4GB(32bit的指针最大存储为2^32=4GB)?
4G-32G之间可以通过算法来优化使JVM能支持更大的寻址空间,32G以上指针压缩失效。
6. 锁优化
6.1 锁升级的过程
在 jdk1.6 之前synchronized被称为重量级锁,这个重指的是它会阻塞和唤醒线程。而1.6之后,对synchonized进行来优化。加入了偏向锁以及轻量级锁。在使用synchronized的过程中,随着竞争的加剧,锁会经历由:无锁->偏向锁->轻量级锁->重量级锁
的升级过程。
问:为什么synchronized要设计一个升级过程呢?
答:阻塞和唤醒线程带来的上下文切换的开销,往往比线程执行的开销要大得多。为了避免这种开销,尽量减少线程阻塞和唤醒的次数。
值得注意的是,锁升级依赖的关键信息为2bit的锁标记位
。
6.2 无锁
无锁状态是对象的最初状态,锁标记为01,是否偏向锁为0。当有一个线程来获取锁的时候进入偏向锁状态。
代码验证
import org.openjdk.jol.info.ClassLayout;
public class Animal {
private Cat cat=new Cat();
private int num=10;
private class Cat{
}
public static void main(String[] args) {
Animal animal=new Animal();
//8+4+4+4+4=24
System.out.println(ClassLayout.parseInstance(animal).toPrintable());
}
}
可以看到是否偏向锁为0,锁标记为01
6.2 偏向锁
- 偏向锁的定义
所谓偏向锁,指的是偏向与某个线程的锁
。研究发现,大部分情况下都是只有一个线程竞争锁
。当初次执行到synchonized的时候,锁由无锁转变成偏向锁,并通过CAS将thread信息写入MarkWord。如果第二次再执行到synchronized,发现偏向的线程id为自身,则可以在无需加锁的情况下获得锁。如果始终都是一个线程获得锁,则执行的效率很高,可以极大提升性能。 - 偏向锁的执行流程
锁获取
1.检查锁信息(MarkWord)。2.如果锁标记(lock)为01,进一步检查是否偏向锁标识(biased_lock),如果为0,则表示是无锁状态。利用CAS将当前线程ID,写入MarkWord,此时锁升级为偏向锁状态
。3.如果锁标记(lock)为01成立,是否偏向锁标识(biased_lock)为1。则进一步检查MarkWord中的thread 是否为自己,如果是则直接获得锁,执行同步块的代码
。4.如果thread 不为自己,则利用CAS将MarkWordz中的Thread置换为自己。如果置换成功,则成功获得锁
,执行同步块代码。5.如果置换不成功,则证明存在竞争。已获得偏向锁的线程在到达全局安全点时会被挂起,然后根据线程状态决定变成无锁或者轻量级锁
。
锁释放
偏向锁的释放,采用了一种有竞争才会释放的策略,执行完同步代码块后并不会主动释放。
1.到达全局安全点时暂停持有偏向锁的线程。2.检查持有偏向锁的线程是否存活。3.线程存活则锁升级为轻量级锁。4.线程不存活,则锁变成无锁状态
。
- 代码验证
public class Animal {
private Cat cat = new Cat();
private int num = 10;
private class Cat {
}
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
new Thread(() -> {
Animal animal = new Animal();
synchronized (animal) {
System.out.println(ClassLayout.parseInstance(animal).toPrintable());
}
}).start();
}
}
由于虚拟机启动后4s左右才会开启偏向锁,顾这里休眠5s
可以看到,相比于无锁状态。当只有一个线程竞争animal的时候,由无锁升级为偏向锁。
6.3 轻量级锁
- 轻量级锁的定义
所谓轻量级锁是相对于重量级锁而言
,当关闭偏向锁或者偏向锁竞争加剧的时候,锁会由偏向锁升级为轻量级锁。轻量级锁会通过一定程度的自旋来尝试获得锁,避免直接升级为重量级锁
。 - 轻量级锁的执行流程
锁获取
1.在栈帧中创建LockRecord,并将MarkWord复制到LockRecord
中(官方称之为Displaced MarkWord)。2.用CAS将MarkWord置换为当前线程LockRecord指针
。3.置换成功,则获得锁,执行同步代码块。4.置换失败,则通过自旋获取锁
。如果获取失败,则膨胀为重量级锁
。
锁释放
1.轻量级锁在执行完同步代码块后会尝试释放锁
。2.释放时用CAS将Displaced MarkWord置换为Mark Word
。3.置换成功,成功解锁。3.置换失败,则唤醒阻塞的线程
。
可以看到,同时有两个及以上线程竞争锁的时候,是直接上升到轻量级锁的状态的。
代码验证
public class Animal {
private Cat cat = new Cat();
private int num = 10;
private class Cat {
}
public static void main(String[] args) throws InterruptedException {
Animal animal = new Animal();
// TimeUnit.SECONDS.sleep(5);
new Thread(() -> {
synchronized (animal) {
System.out.println(ClassLayout.parseInstance(animal).toPrintable());
}
}).start();
}
}
我们把线程睡眠注释掉,让其来不及启动偏向锁,可以发现直接升级为了轻量级锁。
6.4 重量级锁
- 重量级锁的定义
重量级锁是基于Monitor实现的,而Monitor又依赖操作系统层面的Mutex Lock。这种锁会涉及到用户态和内核态的切换,往往开销比较大。所以Jdk 才对synchronized进行了优化,避免一上来就变成重量级锁。 - 重量级锁的执行流程
详见Monitor部分。 - 代码验证
public class Animal {
private Cat cat = new Cat();
private int num = 10;
private class Cat {
}
public static void main(String[] args) throws InterruptedException {
Animal animal = new Animal();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
synchronized (animal) {
System.out.println(ClassLayout.parseInstance(animal).toPrintable());
}
}).start();
}
}
}
我们创建了三个线程直接竞争锁
锁标记为10,即重量级锁
。
7. 总结
本篇文章里,我们为了搞清楚synchronized的原理,先是介绍了锁的分类,再介绍了锁的用法,然后是synchronized的原理,同时为了搞清楚synchronized,我们又初识了Monitor。又因为synchronized的使用过程内含锁升级的过程,为了搞清楚锁升级,我们又介绍了对象内存布局,最后才是锁升级的过程。
可见,为了搞清楚一个问题,牵扯出无数的问题。对于我们使用而言,就是一个synchronized关键字而已,但是却又那么多知识点作为铺垫。学习不易啊,且学且珍惜吧。变强了的同时,也往往伴随着变秃......
篇幅所限,许多细节地方并为介绍。同时由于水平有限,文章中难免有疏漏的地方,欢迎批评指正。我们下篇文章见....
参考文章 Java 并发编程的艺术