原文:《Synchronized 看一篇就够了》
使用场景:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
- 修饰静态方法,作用于当前类加锁,进入同步代码前要获得当前类的锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。
synchronized:解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。
锁是存在对象头中的:
TestClass1的实例中的三块数据分别是什么?
在 JVM 中,对象在内存中分为三块区域:
- 对象头
-
- Mark Word(标记字段):用于存储对象自身的运行时数据,比如对象自己的HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
-
Class Metadata Address(也叫Klass Word。类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
-
- Array length:如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。它的长度规则和压缩规则和klass word相同,64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
- 实例数据
-
- 这部分主要是存放类的数据信息,父类的信息。
- 对齐填充
-
- 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。
- 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
Java对象的状态主要靠Mark Word来标记,以下是Java对象的5种锁状态,大部分与线程有关,下面每一行代表对象处于某状态时Mark Word的样子。
Mark Word(64bit) |
锁状态 |
|||||
unused:25 |
identity_hashcode:31 |
unused:1 |
age:4 |
biased_lock:0 |
lock:01 |
正常/无锁 |
thread:54 |
epoch:2 |
unused:1 |
age:4 |
biased_lock:1 |
lock:01 |
偏向锁 |
ptr_to_lock_record:62 |
lock:00 |
轻量级锁 |
||||
ptr_to_heavyweight_monitor:62 |
lock:10 |
重量级锁 |
||||
空 |
lock:11 |
GC标志 |
上表中各部分和属性,从右往左含义如下:
- lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。biased_lock和lock一起,表达的状态含义如下:
biased_lock lock 状态
0 01 无锁
1 01 偏向锁
00 轻量级锁
10 重量级锁 (synchronized锁指的实际上就是他)
11 GC标记
- age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
- identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向锁的时间戳。
- ptr_to_lock_record:6位指针,轻量级锁状态下,指向线程栈帧中锁记录的指针。
- ptr_to_heavyweight_monitor:重量级锁状态下,指针指向的是Monitor对象(也称为管程或监视器锁)的地址。
我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。
在jdk6 之前,通过synchronized关键字加锁时使用无差别的的重量级锁,重量级锁会造成线程排队(串行执行),并且使CPU在用户态和核心态之间频繁切换。随着对synchronized的不断优化,提出了锁升级的概念,并引入了偏向锁、轻量级锁、重量级锁。在Mark Word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态,并判断出其余位存储的是什么信息。
锁升级步骤:
- 无锁:初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是无锁状态,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
- 偏向锁:当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。这样就完成了从无锁升级为偏向锁的过程。
- 轻量级锁:当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。
- 重量级锁:如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。
了解了锁升级的过程,我们就看看这个重量级锁是个什么东西。前面说了,重量级锁时Mark Word中会有一部分空间记录的是一个Monitor对象的地址。当对象的锁级别升级为“重量级锁”时,JVM就开始采用Object Monitor机制控制各线程抢占对象的过程了。实际上这是JVM对操作系统级别Mutex Lock(互斥锁)的管理过程。
Monitor
什么是Monitor?
Monitor的类名叫做ObjectMonitor,源码参考:ObjectMonitor源码。
- Monitor是一种用来实现同步的工具,他是在Jvm中实现的,用C++编写的。
- 与每个java对象相关联,即每个java对象都有一个Monitor与之对应。
- Monitor是实现Sychronized(内置锁)的基础。
Monitor的基本结构是什么?
- _owner字段:初始时为NULL表示当前没有任何线程拥有该monitor record,每当线程成功拥有该锁后Owner保存当前线程唯一标识,当锁被释放时又设置为NULL。
- _WaitSet:存放处于wait状态的线程队列。
- _EntryList:存放处于等待锁block状态的线程队列。
- _recursions:锁的重入次数。
- _count:用来记录该线程获取锁的次数。
ObjectMonitor的工作过程
ObjectMonitor中通过三个集合来存储不同状态的线程:
- EntrySet:停留在这个区域的线程由于还没有获得对象操作权限的原因,依然停留在synchronized同步块以外,具体来说就是位于synchronized(Object)这句代码的位置,还没有进入到synchronized同步块内。处于“Entry Set”区域的线程,其线程状态被标识为BLOCKED。
- Owner:对象操作权持有区,一个时间点最多有一个线程处于这个区域。也就是说一个时间点只可能有一个线程能拥有这个对象的操作权限。简单点就是当前在synchronized同步块内正在执行的线程在这个集合中。
- WaitSet:某一个线程通过wait等相关方法释放了对象的操作权限,但是只要这个线程没有退出synchronized同步块,就不会释放这个对象的抢占权。这是因为没有退出synchronized同步块,且暂时没有对象操作权限的线程都会被放置到待授权区域(Wait Set)。但是并不是处于待授权区(Wait Set)的线程都可以重新参与对象操作权的抢占,而是只有通过notify()或者notifyAll()相似方法被通知转移的线程能够参与。注意,每个对象的Object Monitor控制过程相对独立,但是一个线程可以同时拥有一个或者多个对象的操作权限。