Java并发编程系列:
一、重量级锁
上篇文章中向大家介绍了Synchronized的用法及其实现的原理。现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
二、轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:
锁状态 |
25 bit |
4bit |
1bit |
2bit |
|
23bit |
2bit |
是否是偏向锁 |
锁标志位 |
||
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
重量级锁 |
指向互斥量(重量级锁)的指针 |
10 |
|||
GC标记 |
空 |
11 |
|||
偏向锁 |
线程ID |
Epoch |
对象分代年龄 |
1 |
01 |
无锁 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
1、轻量级锁的加锁过程
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
图2.1 轻量级锁CAS操作之前堆栈与对象的状态
图2.2 轻量级锁CAS操作之后堆栈与对象的状态
2、轻量级锁的解锁过程:
(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
三、偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
1、偏向锁获取过程:
(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
2、偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
3、重量级锁、轻量级锁和偏向锁之间转换
图 2.3三者的转换图
该图主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。
四、其他优化
1、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
1 package com.paddx.test.string; 2 3 public class StringBufferTest { 4 StringBuffer stringBuffer = new StringBuffer(); 5 6 public void append(){ 7 stringBuffer.append("a"); 8 stringBuffer.append("b"); 9 stringBuffer.append("c"); 10 } 11 }
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
3、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:
1 package com.paddx.test.concurrent; 2 3 public class SynchronizedTest02 { 4 5 public static void main(String[] args) { 6 SynchronizedTest02 test02 = new SynchronizedTest02(); 7 //启动预热 8 for (int i = 0; i < 10000; i++) { 9 i++; 10 } 11 long start = System.currentTimeMillis(); 12 for (int i = 0; i < 100000000; i++) { 13 test02.append("abc", "def"); 14 } 15 System.out.println("Time=" + (System.currentTimeMillis() - start)); 16 } 17 18 public void append(String str1, String str2) { 19 StringBuffer sb = new StringBuffer(); 20 sb.append(str1).append(str2); 21 } 22 }
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。下面是我本地执行的结果:
为了尽量减少其他因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。通过上面程序,可以看出消除锁以后性能还是有比较大提升的。
注:可能JDK各个版本之间执行的结果不尽相同,我这里采用的JDK版本为1.6。
五、总结
本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:
锁 |
优点 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
适用于只有一个线程访问同步块场景。 |
轻量级锁 |
竞争的线程不会阻塞,提高了程序的响应速度。 |
如果始终得不到锁竞争的线程使用自旋会消耗CPU。 |
追求响应时间。 同步块执行速度非常快。 |
重量级锁 |
线程竞争不使用自旋,不会消耗CPU。 |
线程阻塞,响应时间缓慢。 |
追求吞吐量。 同步块执行速度较长。 |
参考文献:
http://www.iteye.com/topic/1018932
http://www.infoq.com/cn/articles/java-se-16-synchronized
http://frank1234.iteye.com/blog/2163142
相关推荐
讨论了监视器锁的加锁过程、锁的状态(偏向锁、轻量级锁、重量级锁)以及锁的膨胀升级过程。此外,文章还覆盖了锁优化技术,例如锁消除和逃逸分析,以及这些技术对Java性能的影响。通过对synchronized关键字的深入...
总的来说,Java的`synchronized`通过对象头的Mark Word和Monitor对象实现了线程安全的同步机制,同时引入了偏向锁、轻量级锁和自旋锁等优化手段,以平衡性能和线程安全性。理解这些锁的工作原理对于编写高性能的并发...
锁升级可以分为四个阶段:无锁、偏向锁、轻量级锁和重量级锁。 4.1、无锁 无锁是指没有锁的状态。 4.2、偏向锁 偏向锁是指 Java 虚拟机在运行时对锁的偏向状态。 4.3、偏向锁流程 偏向锁流程是指 Java 虚拟机...
本文将详细解析Java中的偏向锁、轻量级锁和重量级锁,这些都是JVM为了提高并发性能而实现的锁优化策略。 首先,我们从最简单的偏向锁开始。**偏向锁**的设计理念是假设大多数情况下,锁都不会被多个线程竞争。当一...
在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放...
Java并发编程中的Synchronized是Java实现线程同步的关键机制,其在JDK1.6之后进行了大量的优化,包括引入了轻量级锁和偏向锁,以提升并发性能。以下是关于这些优化的详细解释: **一、重量级锁** 重量级锁是基于...
2. 锁的分类:synchronized 锁可以分为轻量级锁、偏向锁和重量级锁三种。 3. 轻量级锁:轻量级锁是 synchronized 锁的一种实现方式,使用 CAS 操作来更新对象的 Mark Word。轻量级锁的加锁过程可以分为三步:创建锁...
6. **锁优化**:Java提供了许多锁优化策略,如偏向锁、轻量级锁和自旋锁。偏向锁适用于无竞争的场景,轻量级锁用于轻度竞争,而自旋锁则让线程在等待锁释放时保持活跃,避免不必要的上下文切换。 7. **并发调试**:...
3. **线程同步**:书中深入探讨了synchronized关键字的用法,包括可重入锁、锁优化(如偏向锁和轻量级锁)、死锁和活锁等并发问题。此外,还涵盖了java.util.concurrent并发工具包,如Semaphore、CyclicBarrier、...
5. **锁升级**:从无锁到偏向锁,再到轻量级锁,最后到重量级锁的过程,是Java为了优化锁性能的设计策略。 6. **锁优化**: - **减少锁持有时间**:尽量缩短锁的生命周期,降低并发冲突的可能性。 - **减小锁粒度...
- **锁优化**:JVM提供了多种锁优化技术,如轻量级锁、偏向锁等,以减少锁的开销并提高并发性能。 - **线程本地存储(ThreadLocal)**:通过`ThreadLocal`类,可以为每个线程提供独立的变量副本,避免了线程间的共享...
synchronized在实现时有锁升级的机制,包括偏向锁、轻量级锁和重量级锁。偏向锁是默认启用的,它假设同一个锁在同一时间内只有一个线程请求访问,如果在竞争环境下被打破,锁会升级到轻量级锁。在竞争激烈的情况下,...
但在JDK1.6及以后的版本,JVM对`synchronized`进行了多方面的优化,包括引入了CAS操作、偏向锁和轻量级锁等技术,以降低锁的开销。 **1. CAS操作(Compare and Swap)** CAS是一种无锁算法,它试图比较并替换内存中...
- **轻量级锁(Lightweight Locking)**:在没有竞争的情况下,使用轻量级锁代替重量级锁,减少上下文切换。 - **偏向锁(Biased Locking)**:当一个线程连续持有锁时,可以优化为偏向锁,减少锁状态转换的开销。...
- 轻量级锁:当锁不存在竞争时,使用CAS操作升级锁状态,避免了阻塞和操作系统级别的上下文切换。 - 自旋锁:如果锁很快就能释放,线程不会立即阻塞,而是会循环检查是否能获取锁,减少了不必要的阻塞时间。 - 锁...
- 锁优化:分析了JVM对锁的优化,如自旋锁、偏向锁和轻量级锁。 10. **实战案例** - 分布式锁:介绍如何在分布式环境中实现锁,保证多节点间的并发一致性。 - 高并发Web应用:讨论了在高并发Web应用中,如何利用...
`synchronized`在JVM层面是基于监视器锁(Monitor)实现的,依赖于操作系统的Mutex lock(互斥锁),早期版本性能较低,但1.5以后通过一系列优化,如锁粗化、锁消除、轻量级锁、偏向锁和自旋锁等,性能得到了显著提升...
synchronized的优化如偏向锁、轻量级锁和重量级锁等策略使其在某些情况下性能优于ReentrantLock。CAS(Compare and Swap)是一种乐观锁,当冲突发生时才进行重试。在ConcurrentHashMap中,通过sun.misc.Unsafe类提供...
这篇文档主要探讨了Java synchronized的锁机制,包括锁粗化、偏向锁、轻量级锁、重量级锁以及锁升级的过程。 1. 锁粗化: 锁粗化是编译器或运行时环境为了减少锁的使用频率,将多个连续的同步块合并成一个大的同步...
此外,虚拟机层面也有锁优化,如偏向锁、轻量级锁和重量级锁的升级机制,旨在减少锁升级带来的性能开销。错误使用锁的例子可能包括死锁、活锁、饥饿等问题,这些问题需要通过合理设计锁的获取和释放顺序,避免循环...