【一文读懂】 Java并发 - 锁升级原理

简介: Java对象头,锁升级的原因,重量级锁、轻量级锁、偏向锁的原理

要明白锁的原理,首先要知道对象头

Java对象头

在Java中,一个对象一般由两部分组成 :1、对象头 ; 2、对象的成员变量信息

在32位的虚拟机中:

(1)普通对象的对象头长度64bit(8字节):
其中的32bit是Mark Word,另外32位是Klass Word,如下:
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------|-------------------------------------|

(2)数组对象的对象头长度96bit(12自己):
除了Mark Word和Klass Word,还有32bit的 array length,如下:
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|

什么是 Mark Word 什么是 Klass Word 什么是array length ?

  • array length - 很明显这是标明数组长度的字段
  • Klass Word - 对象类型指针,通过该字段标明当前对象所属的类
  • Mark Word - 下面着重介绍

在不同状态下,Mark Word 会有不同的表现形式,对应关系如下:

Normal 就是对象的正常状态,以该状态为例

  • 25位是对象的hashcode
  • 4位是age,在垃圾回收中的分代年龄
  • 1位biased_lock,标明是否是对象锁
  • 2位(01),表明当前的加锁状态

Biased 是偏向锁状态
Lightweight Locked 是轻量级锁状态
Heavyweight Locked 是重量级锁状态
Marked for GC 是在JVM进行GC时的状态

无论是偏向锁、轻量级锁、重量级锁,都是在对象头中的Mark Word做文章

锁升级

在默认情况下,synchronized 会首先使用偏向锁,当发现存在竞争时,就会升级为轻量级锁,当竞争比较激烈时,轻量级锁就会升级为重量级锁
但是我们可以通过添加 VM 参数 -XX:-UseBiasedLocking 手动关闭偏向锁,那么synchronized 会直接进入轻量级锁

为什么会有锁升级?

因为从偏向锁 - 轻量级锁 - 重量级锁,互斥程度会越来越高,但是性能代价也越来越大,所以在一开始,如果资源竞争不是很激烈,应该优先选择程度较轻的锁,以下是《Java并发编程的艺术》一书中的原话

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状
态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏
向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高
获得锁和释放锁的效率,下文会详细分析。

重量级锁原理

操作系统会提供 Monitor 对象,对某个对象加上重量级锁之后,该对象头中的 Mark Word 中就被设置成指向 Monitor 对象的指针 即上图所示的 ptr_to_heavyweight_monitor:30

在Monitor对象中会有两个属性: 1、Owner ;2、EntryList
Owner - 指当前这个对象的锁是由哪个对象持有的
EntryList - 是一个线程队列,当其他线程来竞争该对象锁失败时,就会进入EntryList 中阻塞

因为重量级锁的加锁过程需要使用到操作系统提供的 Monitor 对象,换言之,属于内核级别,因此性能代价是较大的,如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

重量级锁的自选优化

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),当前线程就可以避免阻塞

轻量级锁

每个线程的每个栈帧都会包含一个锁记录的结构

锁记录包含两个部分,其中一个部分用存储上锁对象的 Mark Word,另外一部分是对象指针 Object reference,用于指向上锁对象的地址

上锁过程如下,让锁记录中 Object reference 指向上锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,结果如下

如果cas 失败,有两种情况:

  • 其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,如下:

解锁时有如下两种情况

  • 锁记录的值为 null,表示有重入,这时重置锁记录,表示重入计数减一
  • 锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,如果失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀过程

假设此时Thread-0已经对某个对象上了轻量级锁,现实Thread-1也尝试对改对象加锁,必然加锁失败,进入锁升级

  1. 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  2. 然后自己进入 Monitor 的 EntryList 阻塞

如下图

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 Entry List 中 的Thread-1 线程

轻量级锁缺点

在很多情况下,一个对象在某段时间内,会不停被同一个线程上锁,即不断发生锁重入,这就会频繁的进行Mark Word的cas操作,这也是存在一定性能损耗的,因此诞生了偏向锁来优化

偏向锁

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

比较Normal状态和偏向锁状态的Mark Word:

我们发现Normal状态下 biased_lock 是0,偏向锁的 biased_lock 是1; biased_lock 是偏向锁的开启标志,0表示禁用,1表示开启

而且对于Normal,用了 31bit 去存储对象的hashcode;
而偏向锁状态下不在记录对象的hashcode,而是记录线程编号:初始状态下 54bit 的thread标记都是0,当有线程对该对象上轻量级锁后, 54bit 的thread标记记录下当前线程编号,而且那么线程出了临界区释放了锁,thread不会发生改变,所以当该线程对该对象进行锁重入或者重新上锁时,发现里面的线程编号就是自己,就表示没有竞争,不用 CAS

而如果有另外一个线程要对该对象上锁时,发现当前记录着其他线程的编号,就进行锁膨胀,升级为轻量级锁

目录
相关文章
|
4天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
19 3
|
4天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
25 2
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
1月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
71 3
|
1月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
1月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
69 2