乐观锁 vs 悲观锁
Java线程阻塞的代价
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的接入,需要在用户态与内核态之间切换。这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间、寄存器等,用户态切换至内核态需要传递需要变量、参数给内核,内核也需要保护好用户态在切换时现场,包括一些寄存器的值、变量等,以便内核态调用结束后进行现场恢复。
对象头Mark Word
HotSpot虚拟机中,对象在内存中存储的布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐补充(Padding)。其中对象头(Header)包括两部分:Mark Word和类型指针。
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、偏向线程ID等等,占用内存大小与虚拟机位长一致(32位或64位)。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。下图是32位虚拟机中Mark Word在各个状态下存储的内容:
由此可知锁的状态保存在对象头中,是否偏向锁和锁标志位可以确定对象唯一的锁状态。
偏向锁、轻量级锁、自旋锁、重量级锁
上图中显示Java对象头中涉及到的4种锁,分别是偏向锁、轻量级锁、自旋锁和重量级锁。其中重量级锁属于悲观锁,而偏向锁、轻量级锁、自旋锁属于乐观锁。
1. 偏向锁
顾名思义,偏向锁会偏向于第一个占有锁的线程。如果没有竞争,已经获得偏向锁的线程,在将来进入同步块时不会进行同步操作。如果在运行过程中,其他线程也请求相同的锁时,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,升级为轻量级锁。
偏向锁消除了资源无竞争情况下的同步原语,可以提高程序的运行性能。但如果在竞争激烈的场合,偏向锁反而会增加系统负担,降低程序的运行性能。
偏向锁的获取
- 检查Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态
- 如果为可偏向状态,则检查线程ID是否指向当前线程,如果是,则执行步骤5,否则执行步骤3
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程,然后执行步骤5;如果竞争失败,则执行步骤4
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时,获得偏向锁的线程被挂起(撤销偏向锁,会导致stop the world),偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
- 执行同步代码
偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。如果没有竞争,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(没有字节码正在执行),JVM会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(锁标志位为01)或轻量级锁(锁标志位为00)的状态。
偏向锁开启/关闭(默认启用)
- 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭偏向锁:-XX:-UseBiasedLocking
2. 轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
轻量级锁的加锁过程
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为01,是否偏向锁为0),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如下图
- 将对象头的Mark Word复制到Lock Record中
- 复制成功后,JVM将使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Mark Word。如果更新成功,则执行步骤4,否则执行步骤5
- 如果更新成功,则表示这个线程拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00,即表示对象处于轻量级锁状态。此时线程堆栈与对象头的状态如下图
- 如果更新失败,JVM首先会检查锁对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行。否则说明多个线程竞争锁,当前线程尝试使用自旋来获取锁。如果自旋获取锁成功,则依然处于轻量级锁状态,否则轻量级锁就要膨胀为重量级锁,锁标志位设置为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
轻量级锁的释放
- 通过CAS操作将Lock Record中的Mark Word拷贝(Displaced Mark Word)替换锁对象的stack pointer(指向Lock Record的指针)
- 如果操作成功,则同步完成
- 如果失败,则说明已经有其他线程竞争当前对象锁。在当前线程持有锁时,如果其他线程竞争同一个锁,则竞争线程会修改对象头的Mark Word,将锁标志位设置为10,并更新为指向重量级锁的指针。当前线程(持有锁的线程)尝试自旋等待,如果自旋获取锁成功,则依然处于轻量级锁状态,否则轻量级锁就要膨胀为重量级锁,锁标志位设置为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
3. 自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时线程会停止自旋进入阻塞状态。
JDK1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化。
- 如果平均负载小于CPUs则一直自旋
- 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
- 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
- 如果CPU处于节电模式则停止自旋
- 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
- 自旋时会适当放弃线程优先级之间的差异
JDK1.7后默认启用,无需额外进行设置。
4. 重量级锁Synchronized
Synchronized关键字用于保证同步,它可以把任意一个非NULL的对象当作锁。
- 作用于实例方法时,锁住的是对象的实例(this)
- 当作用于静态方法时,锁住的是Class实例
小结
Synchronized的执行流程:
- 检查Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态
- 如果为可偏向状态,则检查线程ID是否指向当前线程,如果是则表示当前线程处于偏向锁状态,然后执行同步代码
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程,偏向标志位设置为1,锁标志位设置为01,然后执行同步码块
- 如果竞争失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
- 如果替换失败,表示其他线程竞争锁,当前线程尝试自旋获取锁
- 如果自旋成功,则依然处于轻量级锁状态
- 如果自旋失败,则轻量级锁膨胀为重量级锁(monitor),后面等待锁的线程也要进入阻塞状态
锁优化
减少锁的时间
不需要同步的代码,尽量不要放在同步块中执行,以便减少锁的持有时间,尽快释放锁。
减少锁的粒度
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁的竞争。(空间换时间)
Java中很多数据结构都是采用这种方法提高并发操作的效率,比如ConcurrentHashMap中的segment数组(Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁。put操作时先确定往哪个Segment放数据,只需要锁定这个Segment,其它的Segment不会被锁定)、LinkedBlockingQueue(LinkedBlockingQueue在队列头入队,在队列尾出队,入队和出队使用不同的锁)等。
锁粗化
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度。
在以下场景下需要粗化锁的粒度: 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的。
锁分离
锁分离也是一种减小锁粒度的一种,这里强调对锁的功能进行分离,典型的就是读写锁ReentrantReadWriteLock,读操作加读锁,可以并发读;写操作使用写锁,不能并发写,而且写操作时无法获取读锁。读写锁可以在读多写少的系统中提高系统性能。
使用锁分离的数据结构有CopyOnWriteArrayList、 CopyOnWriteArraySet 等容器(即写时复制)、LinkedBlockingQueue等。
锁消除
在即时编译时,如果发现不可能被共享的对象加了锁(逃逸分析),则可以消除这些对象的锁操作。
无锁
锁是悲观操作,而无锁是乐观操作,在竞争不激烈的情况下,效率会比较高。无锁的一种实现方式是CAS(Compare And Swap)操作,默认不进行同步操作,在更新不成功的情况下重试。
参考
- 《深入JVM内核—原理、诊断与优化》系列课程 - 葛一鸣老师
- 【Java对象解析】不得不了解的对象头
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
相关推荐
了解这些状态有助于理解线程的执行流程。 - `Thread.sleep()`方法可以使当前线程进入阻塞状态,到指定时间后自动恢复。 - `join()`方法让调用的线程等待目标线程执行完毕后再继续执行。 2. **`synchronized`...
- **流程控制**:如if语句、switch语句、for循环、while循环等,控制程序执行的流程。 2. **类与对象**: - **类(Class)**:Java中的核心概念,是对象的模板,定义了对象的属性(成员变量)和行为(方法)。 -...
2. **线程同步**:在多线程环境下,数据共享可能导致数据不一致问题,Java提供了多种同步机制,如`synchronized`关键字、`Lock`接口(包括`ReentrantLock`)以及`volatile`关键字,以确保线程安全。 3. **并发集合*...
Synchronized 锁对象可以分为两部分:Java 对象内存布局和 Java 锁结构信息。 3.1、Java 对象内存布局 Java 对象内存布局是指 Java 对象在内存中的存储结构。 3.2、Java 锁结构信息 Java 锁结构信息是指 Java 锁...
Java面试是每位Java开发者在求职过程中必须面对的重要环节。这份"java面试java_interview_guide-master.zip"资源显然是为准备Java面试而设计的,包含了丰富的Java技术知识点和面试常见问题。以下将从Java语言基础、...
这得益于Java虚拟机(JVM)的存在,它负责解释执行Java字节码。 在基础篇中,你会学习到如何创建和运行你的第一个Java程序,即经典的"Hello, World!"示例。接着,你会了解到变量、数据类型、运算符、控制流程语句...
- **JDBC**:Java连接数据库的基本步骤,包括加载驱动、建立连接、执行SQL等。 - **预编译语句**:PreparedStatement的使用,防止SQL注入。 9. **PPT课件** - 课件通常会包含每个章节的重点讲解,图表示例,以及...
Java的基础包括语法结构、数据类型、变量、运算符以及控制流程。数据类型分为基本类型(如int、char、boolean)和引用类型(如类、接口和数组)。变量是用来存储数据的容器,而运算符则用于执行数学或逻辑操作。控制...
Java提供了Thread类和Runnable接口来实现并发执行。理解线程同步、互斥、死锁等概念,并学会使用synchronized关键字、wait()、notify()方法,是编写多线程程序的关键。 最后,Java SE还包含了网络编程、反射、枚举...
9. **多线程**:Java内置对多线程的支持,可以通过实现Runnable接口或继承Thread类来创建线程,理解并发执行的概念和同步控制(如synchronized关键字、wait/notify机制)。 10. **文件和文件系统操作**:Java提供了...
此外,还会介绍Java程序的基本结构,如类、对象、变量和方法的定义,以及控制流程语句,如if条件判断、for循环和while循环。 接下来,书中深入讲解了面向对象编程(OOP)的概念。Java是OOP的典型代表,它支持封装、...
1. **Java基础**: 书中每个章节的代码示例都是为了讲解Java的基础语法,如变量声明、数据类型、运算符、流程控制(if、switch、for、while)、函数定义与调用、数组等。通过实际运行这些代码,读者可以直观感受Java...
1. **Java基础知识**:涵盖Java语法基础,如数据类型、变量、运算符、流程控制(if-else,switch,循环)、方法定义和调用,以及类和对象的概念。 2. **面向对象编程**:深入讲解封装、继承和多态,这是Java的核心...
15. **JDBC**:Java数据库连接(JDBC)是Java访问数据库的标准API,学习如何连接数据库、执行SQL语句和处理结果集。 陈国君老师的课件很可能会结合实例和练习,帮助学生深入理解和掌握上述知识点,从而提升他们的...
3. Java语法基础:学习基本的数据类型、变量、运算符、流程控制语句(如if-else,switch,for,while)以及方法定义。 二、面向对象编程 4. 类与对象:理解类的定义、对象的创建与使用,掌握封装、继承和多态等面向...
1. **基础语法**:这部分包括变量、数据类型、运算符、流程控制语句(如if、switch、for、while)、方法定义与调用等基本概念,是学习Java的第一步。 2. **面向对象编程**:Java是一种面向对象的语言,系统会测试...
- 线程是程序的执行流程,Java支持多线程编程,可以使用Thread类或实现Runnable接口创建线程。 - 线程同步:synchronized关键字、wait()、notify()等机制防止并发问题。 7. **网络编程** - Java提供了Socket和...
第一部分的学习可能已经让学员们对Java的基本概念有了初步了解,如变量、数据类型、运算符、控制流程(如if语句、for循环和while循环)以及函数的使用。第二部分则在此基础上进行拓展,可能包括以下几个关键知识点:...
最后,Java的并发编程是另一个重要领域,线程、同步机制(如synchronized关键字、Lock接口)、并发集合(如ConcurrentHashMap)等内容都是高并发环境下必备的知识。 总的来说,这个"Crazy JAVA mind map"思维导图将...
Java提供了线程的创建、调度和同步控制机制,如synchronized关键字、锁、条件变量等。 13. Java语言的编译和运行流程:Java程序的开发流程包括源代码编写、编译和运行三个主要步骤。Java源代码文件(.java)通过...