Java并发机制的底层实现原理–synchronized
synchronized的应用
关键词:重量级锁,偏向锁,轻量级锁,java对象头
问题:什么是“锁”?
1.锁。java中每一个对象都可以作为锁。
1.1,普通同步方法,锁是当前实例对象
1.2,静态同步方法,锁是当前class对象
1.3,同步方法块,锁是Synchronized括号里配置的对象
问题:“锁”有什么作用?
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放 锁。
问题:得到“锁”是得到锁住的这个对象吗?
不是。JVM是基于进入和退出Monitor对象来实现方法的同步和代码块的同步的。代码块的
同步是使用monitorenter(插入同步代码块的开始位置)和monitorexit(插入同步代码块的
结束和异常处)指令实现的,而方法同步是使用另一种方式实现的,细节JVM里面没有详细
说明。JVM保证每个monitorenter都有对应的monitorexit进行配对,任何一个对象都有一个
monitor与之管关联,当一个monitor被持有后,它将处于被锁定状态,线程得到
monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。
得到“锁”--->得到monitor的所有权
问题:“锁”存放在哪里?
Synchronized用的锁存在java对象头里,在32位虚拟机中,1字宽等于4字节(32bit)。如
果对象是数组类型,在该虚拟机中用3个字宽存储对象头。非数组类型,用2字宽存储对象
头。
问题:“java对象头”中存放着的是什么信息?
非数组对象:Mark Word(1字宽),Class Metadata Address(1字宽)
数组对象:非数组对象基础上+(Array length(1字宽))
其中Mark Word存放有锁信息。
问题:Mark Word是什么结构?
32bit结构如下:
锁状态 | 25bit | 4bit | 1bit是否偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象的分代年龄 | 0 | 01 |
64bit结构如下
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
无锁 | unuse | hashCode | 0 | 01 | ||
偏向锁 | ThreadID(54bit)Epoch(2bit) | 1 | 01 |
32bit结构的Mark Word状态变化如下
锁状态 | 25bit | 4bit | 1bit | 2bit |
---|---|---|---|---|
轻量级锁 | 记录栈中锁记录的指针(30bit) | 00 | ||
重量级锁 | 指向互斥量(重量级锁)的指针(30bit) | 10 | ||
GC标记 | 空(30bit) | 11 | ||
偏向锁 | 线程ID(23bit)Epoch(2bit) | 分代对象年龄(4bit) | 1 | 01 |
锁的升级与对比
前言: JavaSE1.6中,锁一共有4中状态,级别从低到高依次。无锁状态,偏向锁
状态,轻量级锁状态,重量级锁状态。锁会随着竞争状况逐渐升级(不能降级)
问题:偏向锁诞生的原因?
大多数情况下,锁不仅不存在多线程竞争,而且总是同一线程多次获得。
问题:偏向锁的作用?
当一个线程访问同步代码块并获取锁时,会在对象头和栈帧的锁记录存储该线
程ID,以后该线程进入和退出同步代码块时不需要进行加锁和解锁操作,只需
检测对象头里的Mark Word里是否存储着指向当前线程的偏向锁(成功–>获得
锁)
问题:轻量级锁诞生的原因?
当应用程序里的锁绝大多数处于竞争关系时,偏向锁每次处理这样的线程都需
要切换偏向锁的值,效率产生影响,
锁的优缺点对比
锁 | 优点 | 缺点 | 适用范围 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程之间存在锁竞争,会带来额外的锁撤销消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋消耗CPU | 追求响应时间同步执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步执行速度较长 |