多线程进阶:synchronized底层原理,锁优化、锁升级的过程

文章目录

  • 一、synchronized底层原理
      • Java对象组成
      • MarkWord
  • 二、JVM对synchronized的优化
      • 锁消除
      • 锁粗化
      • 锁升级
  • 三、锁升级的过程
      • 偏向锁
      • 轻量级锁
      • 重量级锁
      • 优缺点


提示:以下是本篇文章正文内容,Java系列学习将会持续更新

一、synchronized底层原理

Java对象组成

我们都知道对象是放在堆内存中的,对象大致可以分为三个部分,分别是对象头,实例变量和填充字节

多线程进阶:synchronized底层原理,锁优化、锁升级的过程_第1张图片

MarkWord

synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么synchronized锁对象是存在哪里的呢?
答案是存在锁对象的对象头Mark Word,而Mark Word中存储了两个信息:是否偏向锁、锁标志位
多线程进阶:synchronized底层原理,锁优化、锁升级的过程_第2张图片
原理

在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做出的优化,以及锁升级的具体过程。之后的学习内容将持续更新!!!

你可能感兴趣的:(Java多线程与并发,jvm,java,开发语言)