提示:以下是本篇文章正文内容,Java系列学习将会持续更新
一、synchronized底层原理
Java对象组成
我们都知道对象是放在堆内存中的,对象大致可以分为三个部分,分别是对象头,实例变量和填充字节
MarkWord
synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么synchronized锁对象是存在哪里的呢?
答案是存在锁对象的对象头Mark Word,而Mark Word中存储了两个信息:是否偏向锁、锁标志位
原理:
在Java中,对象是在堆内存上的,而对象中有一个对象头,对象头中有MarkWord用于存储锁的信息。
MarkWord中有两个标志:是否偏向锁、锁标志位(无锁/ 轻量级锁/ 重量级锁/ GC标记)
锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。synchronized给对象加锁就是修改对象头中这两个锁标志位的数值。
当一个Java对象上锁之后,就会指向一个Monitor对象(监视器),当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。 monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/ notifyAll/ wait等方法存在于Object中的原因
二、JVM对synchronized的优化
锁消除
编译器+JVM判断是否需要消除锁:在单线程的情况下,或者多线程但不存在竞争锁的情况,就会消除锁以提高效率。
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
String str = sb.toString();
锁粗化
一段逻辑中如果出现多次频繁的加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
public class Demo {
// 粒度比较细
public void test1() {
Object o = new Object();
for(int i = 0; i < 100; i ++){
synchronized(o){
System.out.println("执行任务");
}
}
}
// 适当粗化
public void test2() {
Object o = new Object();
synchronized(o){
for(int i = 0; i < 100; i ++){
System.out.println("执行任务");
}
}
}
}
锁升级
synchronized
锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁,这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态
无锁 < ——> 偏向锁 ——> 轻量级锁 ——> 重量级锁
三、锁升级的过程
偏向锁
当不存在锁竞争的,经常是一个线程多次获得同一个锁的情况下,如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
核心思想:
如果一个线程
第一次加锁
,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了。偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
轻量级锁考虑的是竞争锁对象的
线程不多
,而且线程持有锁的时间也不长
的情景。
因为阻塞线程需要高昂的耗时实现CPU从用户态转到内核态
的切换,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
该线程在自旋过程中,会不断的请求锁。如果多次请求失败,就会膨胀到重量级锁。
重量级锁
加锁机制重度依赖了 OS 提供了 mutex锁,申请锁失败,需要放弃CPU,进入阻塞状态
· 大量的内核态用户态切换
· 很容易引发线程的调度
重量级锁通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
优缺点
锁状态 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别 | 如果竞争的线程多,那么会带来额外的锁撤销的消耗 | 基本没有线程竞争锁的同步场景 |
轻量级锁 | 竞争的线程不会阻塞,使用自旋,提高程序响应速度 | 如果-直不能获取锁,长时间的自施会造成CPU消耗 | 适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景 |
重量级锁 | 线程竞争不适用CPU自旋,不会导致CPU空转消耗CPU资源 | 线程阻塞 ,响应时间长 | 很多线程竞争锁,且锁持有的时间长,追求吞吐量的场景 |
总结:
提示:这里对文章进行总结:
以上就是今天的学习内容,本文是Java多线程的学习,了解了synchronized的底层原理,JVM对syn做出的优化,以及锁升级的具体过程。之后的学习内容将持续更新!!!