问题分析
i++的JVM字节码指令
三个步骤,非原子的操作
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增
i—的JVM字节码指令
三个步骤,非原子的操作
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
isub // 自减
临界区
- 在多个线程对共享资源读写操作实发生指令交错,就会出现问题
- 一段代码块,如果存在对共享资源的多线程读写操作,这段会发生指令交错的代码块:临界区
其资源成为临界资源
private static int counter = 0;//临界资源
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter--;
}
竞态条件(Race Condition)
多个线程在临界区执行,由于代码的执行序列不停而导致结果无法预测,称为发生了竞态条件
就是代码胡乱执行了
防止竞态的发生的手段阻塞式的解决方案: synchronized Lock
- 非阻塞式的: 原子变量 CAS范式
注意
Java互斥和同步都可以使用Synchronized实现,但是有区别
互斥:保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区
同步:由于线程执行先后不同,需要一个线程等待其他线程运行到某个点
Synchronized的使用
synchronize:同步块 Java提供的一种原子性内置锁
Java对象都可以把它当做同步锁使用,
Java内置的使用者看不到的锁称为内置锁,也叫监视器锁
加锁方式:
//方式一: 在方法上
public static synchronized void increment() {
counter++;
}
//方式二: 在代码块上
public static void increment() {
synchronized (lock){
counter++;
}
}
Synchronized的底层原理
synchronized:JVM内置锁,基于Monitor机制实现,依赖底层系统互斥的原语mutex(互斥量)
1.5之前:重量锁,
1.5之后:优化了,锁粗化lock coarsening 锁消除,轻量级锁,偏向锁,自适应自旋技术引入,性能与lock持平
synchronized的字节指令
Monitor(管程/监视器)
管程:管理共享变量,以及对共享变量操作的过程,让他们支持并发。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分
三种不同的管程模型:
- Hasen模型
- Hoare模型
- HESA模型
Mesa 模型
引入条件变量概念:每个条件变量对应一个等待队列。
条件变量和等待队列的作用是解决线程之间的同步问题
wait的正确姿势
唤醒的时间和获取到锁继续执行的时间不一致,while(条件不满足) {
wait();
}
被唤醒的线程再次执行可能条件不满足,所有循环检验条件
MESA的wait方法引入超时参数,皮面线程进入等待队列永久阻塞
notify 和notifyAll分别何时使用
notify:
- 所有等待线程拥有相同的等待条件
- 所有线程被唤醒后,执行相同的操作
- 只需要唤醒一个线程
不清楚的话就用notifyAll
Java与Monitor
对MESA模型精简了
Java中的管程只有一个条件变量
monitor在Java中的实现
Java中Object 定义了 wait, notify , notifyAll依赖ObjectMonitor 底层jvm基于C++实现之
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
获取时:当前线程插入到cxq的头部,释放时,默认策略 QMode=0 :
如果entryList为空,从cxq中按原顺序插入到 entryList并唤醒第一个线程,后来先得锁
不为空是:从entrylist去,就是先来先得
对象的内存布局
对象可分为:三块:对象头,实例数据,对齐填充
- 对象头:hash码,对象所属年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数据长度等
- 实例数据:存放类的属性数据信息,包括父类的属信息
- 对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍,不是必须存在的
对象头的详解
对象头包括
- Mark Word
对象自身的运行时数据
如:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,区别32位操作系统还是64位操作系统 - Klass Pointer
指针,指向他的类元数据的指针,可以找到这个对象是那个类的实例
32位4字节,64位开启指针压缩是4字节,否则8字节,JDK1.8默认开启压缩,
- 数据长度
如果对象是个数组会记录数组的长度,4字节
JOL工具
//meaven 依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.10</version>
</dependency>
使用方法
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
- 查看打印的信息 默认开启指针压缩的场景下
- 关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops 作为了解
MarkWork如何记录锁状态的
记录markoop实现Mark Word 具体实现是Markoop.hpp
- hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
- age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
32位JVM下对象结构描述
64位jvm下对象结构描述
Mark word的锁标记的枚举
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
}
使用Jol工具查看锁状态
偏向锁
对加锁的优化,前提假设不存在竞争,总是由同一个线程获取,消除无竞争下的锁重入,通过引入CAS
jvm启用偏向锁模式,新创建对象的Mark Word ThreadId 为0:处于偏向但未偏向任何线程,也叫做匿
名偏向状态
偏向锁延迟偏向
启动4s后才会对新建的对象开启偏向锁模式
因为有一系列复杂的活动,装载,初始化等这些过程会用到Synchronize加锁,为了减少初始化时间
默认延时加载偏向锁
//关闭延迟开启偏向锁
‐XX:BiasedLockingStartupDelay=0
//禁止偏向锁
‐XX:‐UseBiasedLocking
//启用偏向锁
‐XX:+UseBiasedLocking
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread1").start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
调用HashCode的影响:
obj.hashCode()或System.identityHashCode(obj) 会捯饬带对象的偏向锁被撤销
原因:一个对象hashcode只会成成一次,偏向锁没有地方保存hashcode
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
当对象处于可偏向和以偏向下,调用hashcode计算会使对象无法偏向
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
偏向锁撤销之调用wait/notify
偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁
synchronized (obj) {
// 思考:偏向锁执行过程中,调用hashcode会发生什么?
//obj.hashCode();
//obj.notify();
try {
obj.wait(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
轻量级锁
偏向锁事变,会升级为轻量级锁
轻量级锁适用的场景是线程交替执行同步块的场合,
如果多个线程访问同一把锁,轻量级会升级到重量级
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread1").start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
思考: 轻量级锁是否可以降级为偏向锁?
不会,会直接为无锁,再从无锁开始新一轮加锁过程
没有所谓的锁降级一说,直接就是无锁
https://www.jianshu.com/p/9932047a89be
测试:锁升级场景
@Slf4j
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
//obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName() + "开始执行。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 思考:偏向锁执行过程中,调用hashcode会发生什么?
//obj.hashCode();
log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + "释放锁。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
//控制线程竞争时机
Thread.sleep(1);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread2");
thread2.start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
偏向锁—-> 轻量级锁 ——> 无锁
@Slf4j
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread1").start();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread2").start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}