【并发编程】synchronized的偏向锁、轻量级锁、重量级锁详解

内存布局对应对应的锁状态

内存布局对应对应的锁状态.png

先说锁状态的变化结论

锁状态的变化流程图.png

偏向锁

  • 偏向锁是一种针对加锁操作的优化手段。
  • 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。
  • 对于没有锁竞争的场合,偏向锁有很好的优化效果。
  • JVM启用了偏向锁模式:jdk6之后默认开启
  • 新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁延迟偏向

  • HotSpot 虚拟机在启动后开启偏向锁模式默认在4s后。
  • 为了减少初始化时间,JVM默认延时加载偏向锁。
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 

延迟开启偏向锁.png

  • 上图的代码可以验证:从无锁变为偏向锁(4秒)

偏向锁在无竞争的时候一直是偏向锁

public static void main(String[] args) throws InterruptedException {
		log.debug(Thread.currentThread().getName() + "最开始的状态。。。\n"
				+ 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(Thread.currentThread().getName() + "结束状态。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
	}

  • 执行结果

偏向锁在无竞争.png

  • 从结果可以看出:无锁状态经过4秒变为偏向锁,之后的的状态一直是偏向锁!
  • 在进入同步代码块后,锁的偏向线程由0变为具体的线程。

在同步代码块外调用hashCode()方法

同步代码块外调用hashCode()结果.png

  • 进入同步代码块后锁升级为轻量级锁
  • 当对象可偏向(线程ID为0)时,MarkWord将变成未锁定状态,并只能升级成轻量锁。

在同步代码块内调用hashCode()方法

同步代码块内调用hashCode()结果.png

  • 直接升级为重量级锁
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

偏向锁撤销:自己验证wait和notify

  • 调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。
  • 因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。
  • 轻量级锁会在锁记录中记录 hashCode。
  • 重量级锁会在 Monitor 中记录 hashCode。
  • 当对象可偏向(线程ID为0)时,MarkWord将变成未锁定状态,并只能升级成轻量锁。
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
  • 偏向锁状态执行obj.notify() 会升级为轻量级锁。
  • 调用obj.wait(timeout) 会升级为重量级锁。

轻量级锁

  • 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。
  • 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
  • 轻量级锁在降级的时候直接变为无锁状态!(查看之前在同步代码块外调用hashCode()方法)

模拟竞争不激烈的场景

@Slf4j
public class TestMemory {

	public static void main(String[] args) throws InterruptedException {
		log.debug(Thread.currentThread().getName() + "最开始的状态。。。\n"
				+ ClassLayout.parseInstance(new Object()).toPrintable());
		// HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
		Thread.sleep(4000);
		Object obj = new Object();

		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				log.debug(Thread.currentThread().getName() + "开始执行thread1。。。\n"
						+ ClassLayout.parseInstance(obj).toPrintable());
				synchronized (obj) {
					log.debug(Thread.currentThread().getName() + "获取锁执行中thread1。。。\n"
							+ ClassLayout.parseInstance(obj).toPrintable());
				}
				log.debug(Thread.currentThread().getName() + "释放锁thread1。。。\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() + "开始执行thread2。。。\n"
						+ ClassLayout.parseInstance(obj).toPrintable());
				synchronized (obj) {
					log.debug(Thread.currentThread().getName() + "获取锁执行中thread2。。。\n"
							+ ClassLayout.parseInstance(obj).toPrintable());
				}
				log.debug(Thread.currentThread().getName() + "释放锁thread2。。。\n"
						+ ClassLayout.parseInstance(obj).toPrintable());
			}
		}, "thread2");
		thread2.start();

		Thread.sleep(5000);
		log.debug(Thread.currentThread().getName() + "结束状态。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
	}
}


竞争不激烈的场景的运行结果

轻量级锁结果.png

重量级锁

  • 轻量级锁经过一次自选如果没有获取到锁,直接膨胀为重量级锁。
  • 重量级锁是基于 Monitor 机制,并且在 Monitor 中记录 hashCode

模拟竞争激烈的场景

  • 去掉不激烈的场景中的以下代码就是竞争激烈的场景
// 控制线程竞争时机
Thread.sleep(1);

竞争激烈的场景的运行结果

重量级锁结果.png

结束语

  • 获取更多有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 关注公众号,后续持续高效的了解spring源码!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
posted @ 2022-01-30 14:57  程序java圈  阅读(885)  评论(1编辑  收藏  举报