引言
在学习synchronized前,我们实际上是需要先了解Java对象在jvm中的储存结构,在了解了它的实际储存方式后,再对后边的锁学习会有一个更好更深入的理解。
一、对象结构
-
我们为什么要知道什么是对象头
-
在学习synchronized的时候,所涉及到的大部分原理都是与对象头相关,所以理解对象头是我们深入理解synchronized的前提条件。
-
-
什么是对象头
我们编写一个
Java
类,编译后会生成.class
文件,当类加载器将class
文件加载到jvm
时,会生成一个Klass
类型的对象(c++
),称为类描述元数据,存储在方法区中,即jdk1.8
之后的元数据区。当使用new
创建对象时,就是根据类描述元数据Klass
创建的对象oop
,存储在堆中。每个java
对象都有相同的组成部分,称为对象头。 -
如何查看对象头,jol-core 查看对象头的神器(Java对象布局)
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version> </dependency>
-
查看某个对象的布局
public class User { private String name;private Integer age; public User(String name, Integer age) {this.name = name;this.age = age;} public String getName() {return name;} public void setName(String name) {this.name = name;} public Integer getAge() {return age;} public void setAge(Integer age) {this.age = age;} } User a = new User("小明", 12); System.out.println(ClassLayout.parseInstance(a).toPrintable());
输出:
代表含义:
OFF:偏移量,通常指的是指定再数据结构中的某个元素的位置。它表示从起始位置偏移多少个字节或者多少个元素才能到达目标元素所在的位置。例如上图中的object header,mark占了8字节,由0开始,class就要在8的位置开始查找位置。
SZ: 代表大小,所占空间大小,字节单位
TYPE DESCRIPTION:类型描述,object header 为对象头
VALUE: 当前内存中存储的值。
-
object header中对象当前的状态
-
当无锁状态下
-
前25位bit位是未使用的,hashcode占了31bit,中间的1bit位是未使用的,4bit位是分代年龄,最后的3bit位是 001 表示无锁状态
-
-
如果是偏向锁的状态
-
前54bit位是当前线程指针,2bit位是Epoch,1bit位未使用,4bit位分代年龄,剩余3bit位分别是,101 表示锁定状态
-
-
如果是轻量锁状态
-
前62位指向lock record指针 最后的锁类型为 00
-
-
如果是重量锁
-
前62位指向互斥锁指针,锁类型位10
-
-
GC标记
-
前62位为空,锁类型为11
-
-
-
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充
-
对象头占 4字节,共64bit位,在jvm源码中对应mark-word 对象,主要用于存储一些列的标记位,比如哈希值、轻量级锁的标记位、偏向锁标记位、分代年龄
-
Klass Pointer:Class对象的类型指针,Jdk1.8默认开启指针压缩后为4字节,关闭指针压缩后长度为8字节。其指向的位置是对象对应的Class对象的内存地址。
-
对象实际数据:包括对象的所有成员变量,大小由各个成员变量决定
-
对齐: 这段对齐是非必须的,由于HotSpot虚拟机的内存管理系统要求对象起始地址必须是8字节的整数倍,如果实例数据对象没有对齐的划,就需要对齐占位来补全。
-
-
在mark-word锁类型标记中,无锁、偏向锁、轻量锁、重量锁、以及GC标记,五种类中没法用2比特位标记,所以无锁、偏向锁又往前占了以为偏向锁标记,最终:001为无锁、101为偏向锁。
二、synchronized
-
关于synchronized的锁优化升级膨胀最终修改的就是对象头中最后三位的标识来区分不同的锁类型,从而采取不同的策略来提升性能。
为什么第二个对象里的HashCode展示和 下方列出的不一样
-
倒过来是因为大小端存储导致:
-
Big-Endian:高位字节存放于内存的低地址端,低位字节存放于内存的高地址端
-
Little-Endian:低位字节存放于内存的低地址端,高位字节存放于内存的高地址端
-
-
-
Monitor对象
概念:在HotSpot虚拟机中,monitor是由C++中ObjectMonitor实现。synchronized的运行机制,就是当JVM检测到对象在不同的竞争状态时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。那么三种不同的Monitor实现,也就是常说的三种不同的锁:偏向锁、轻量级锁和重量级锁。当一个Monitor被某个线程持有后,它便处于锁定状态。
-
源码地址
ObjectMonitor() {_header = NULL;_count = 0; // 记录个数_waiters = 0, _recursions = 0; // 线程重入次数_object = NULL; // 存储monitor对象_owner = NULL; // 持有当前线程的owner_WaitSet = NULL; // 处于wait状态的线程,会被假如到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ; // 单向列表FreeNext = NULL ;_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
-
每个 Java 对象头中都包括 Monitor 对象(存储的指针的指向),synchronized 也就是通过着一种方式获取锁,也就解释了synchronized() 括号里放任何对象都能获得锁。
-
2.1 synchronized特性
-
原子性
原子性是指一个操作不可中断的,要么全部执行成功,要么全部执行失败。
synchronized可以保证统一时间只有一个线程能拿到锁,进入到代码块执行。
-
代码:
public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {for (int i1 = 0; i1 < 10000; i1++) {add();}});thread.start();}Thread.sleep(1000);System.out.println(counter); } public static void add() {synchronized (AtomicityTest.class) {counter++;} }
-
反编译:
可以看到add指令里的monitorenter就是表示先去获取对象的monitor,monitorexit就表示使用结束退出,由其他线程开始竞争锁。
指令逻辑:
-
每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
-
当同一个线程再次获得该monitor的时候,计数器再次自增;
-
当不同线程想要获得该monitor的时候,就会被阻塞。
-
-
当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。
-
-
可见性
通过synchronized也能保持可见性。
和volatile的区别在于,volatile是通过内存屏障来实现的可见性,而synchronized实现
-
线程解锁前必须把共享变量的最新值刷新到主内存中
-
加锁前必须清空工作内存中共享变量的值,从而使用共享变量事需要从主内存中重新读取最新的值。
-
synchronized靠系统内核互斥锁实现。退出代码块时刷新变量到主内存。
-
-
有序性
as-if-serial,保证不管编译器和处理器为了性能优化会如何进行指令重排序, 都需要保证单线程下的运行结果的正确性。也就是常说的:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。
也就是说,在使用了synchronized后,会发生指令重排的可能,比如设计模式中的单例模式,假如仅是做了双重判空和synchronized加锁,可能会导致多线程获取实例不同。比如,单例模式中的双重检查锁,增加volatile的目的就是为了防止指令重排,以防多线程的情况下,A线程执行到了指向内存地址的步骤,B线程就获取到了锁从而导致new出了多个对象的问题,失去了单例模式的本意。
public class Singleton {private Singleton() {};private volatile static Singleton instance;public Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {// B线程进入判断,此时的instance还是为空的instance = new Singleton(); // 执行new之后代码块结束,monitorexit指令退出,下一个线程已经可以获取到锁,开始指向内存地址。}}}return instance;} }
-
可重入性
synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁的临界资源,这种情况称为可重入锁。
为什么需要可重入锁,就是因为for循环调用时可能会发生当前线程循环请求已经拥有的锁,实现逻辑就是在monitor里存了一个计数器,每当当前线程获取到一次计数器就+1,退出一次就-1直到清零释放锁。
2.2 锁升级过程
-
锁的四种状态:无锁、偏向锁、轻量级锁、重量级锁。这四种锁只能随着顺序升级,不能降级。
-
关于每个锁的状态的存储结构可以看synchronized浅读解析一里的object header部分。
-
锁的概念:
-
无锁
-
无锁是指没有对资源进行锁定,所有线程都能访问并修改同一个资源。但同时只有一个线程能修改成功。
-
-
偏向锁
-
初次执行到synchronized代码块的时候,锁对象会编程偏向锁,字面的意思就是偏向第一个获得它的线程的锁。
-
指的就是当同一段同步代码一直被同一个线程所访问时,既不存在多个线程的竞争时,那么这个线程在后续访问时会自动获得锁,从而降低锁带来的消耗,既提高性能。
-
没有线程之间的切换消耗,所以性能极高。
-
-
轻量锁(自旋锁)
-
大概概念就是当同步代码块都处于无锁状态,但同时有两个线程都在争夺锁,线程1争取成功了,线程2就会失败进入自旋状态。为避免同步代码块执行时间短,可以快速切换到另一个线程的情况,线程2会进入一段时间的自旋状态,既一段无意义的for循环。假如for循环10次后还是获取不到锁,就会升级到下一个状态。
-
-
重量级锁
-
当自旋结束后还未获取到锁,就会膨胀为重量级锁,阻塞住线程。重量级锁十分消耗性能。
-
-