Java并发专题:synchronized
在多线程并发编程中synchronized一直是元老级角色,我们在开发过程中可以使用它来解决线程安全问题中提到的原子性,可见性,以及顺序性。很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
一、synchronized的三种应用方式:
synchronized有三种方式来加锁,分别是:
- 方法锁
在方法锁层面可以有如下3种方式:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 对象锁synchronized(this)
- 类锁synchronized(Demo.Class)。
synchronized括号后面的对象:
synchronized扩号后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
synchronized的字节码指令:
先看 demo 程序:
public class Demo { private static int count = 0; public static synchronized void inc() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(() -> Demo.inc()).start(); } Thread.sleep(3000); System.out.println("运行结果" + count); } }
通过javap -v 来查看对应代码的字节码指令:

又看到了熟悉的东西:ACC_SYNCHRONIZED。对于同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。
二、synchronized的锁的原理:
jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁;了解synchronized的原理我们需要明白3个问题:
- synchronized是如何实现锁
- 为什么任何一个对象都可以成为锁
- 锁存在哪个地方?
在了解synchronized锁之前,我们需要了解两个重要的概念,一个是对象头、另一个是monitor。
2.1、Java对象头:
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键
Mawrk Word:
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),下面就是对象头的一些信息:

在源码中的体现:
如果想更深入了解对象头在JVM源码中的定义,需要关心几个文件,oop.hpp/markOop.hpp 。
oop.hpp,每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应。先在oop.hpp中看oopDesc的定义:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;//理解为对象头
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass; //默认开启压缩
} _metadata;
......
_mark 被声明在 oopDesc 类的顶部,所以这个 _mark 可以认为是一个 头部, 也就是上面那个图种提到的头部保存了一些重要的状态和标识信息,在markOop.hpp文件中有一些注释说明markOop的内存布局:
// Bit-format of an object header (most significant first, big endian layout below): // // 32 bits://对应的上图的头部信息的分布 // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // 64为虚拟机中的分布 // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block) //
Java 中提供了一个jar包给我来查看对象头信息:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
然后编写一个测试类:
// oop.hpp 源码-XX:-UseCompressedOops 关闭压缩指针
//打开偏向锁 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
synchronized (classLayoutDemo) {
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
默认情况下输出以下信息:

这里涉及到大端存储跟小端存储的概念:大端存储与小端存储模式主要指的是数据在计算机中存储的两种字节优先顺序。小端存储指从内存的低地址开始,先存储数据的低序字节再存高序字节;相反,大端存储指从内存的高地址开始,先存储数据的高序字节再存储数据的低序字节。这里正是用这种规则来存储的。
可以看到这里的是经过压缩的,因为上面中提到 JVM虚拟机中 _compressed_klass 默认开启压缩。可以通过 -XX:-UseCompressedOops 关闭压缩指针。然后打印出来的对象头就是128位的。
在上面JVM源码中对对象头的描述中,在64位虚拟机的布局中,最后的两位标识锁的类型,倒数第三位标识是否获得偏向锁,因为JDK1.8中默认是关闭偏向锁的 ,这里看到的是轻量级锁的标识,要知道,偏向锁是用来在没有线程竞争的时候减少性能开销通过自旋获得锁,但是实际的场景下还是存在线程的竞争的,所以默认是关闭的。但是我们可以通过 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 开启偏向锁,然后打印出来的数据就是这样的:

因为我们这里没有竞争,如果存在竞争,就会有不一样的输出 就比如最后3位为 010 ,表示重量级锁
另外需要注意的是在偏向锁下,对象头内是没有空间存储对象的 哈希码的,那么这个时候我们修改一下Demo
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
synchronized (classLayoutDemo) {
System.out.println(classLayoutDemo.hashCode());
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
这个时候输出的信息锁的类型变成了重量级锁了。

Monitor:
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。从源码层面看一下monitor对象
Ø oop.hpp下的oopDesc类是JVM对象的顶级基类,所以每个object对象都包含markOop
class oopDesc {//顶层基类
friend class VMStructs;
private:
volatile markOop _mark;//这也就是每个对象的mark头
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
Ø markOop.hpp 中 markOopDesc继承自oopDesc,

并扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象:这个ObjectMonitor 其实就是对象监视器

Ø objectMonitor.hpp,在hotspot虚拟机中,采用ObjectMonitor类来实现monitor:

到目前位置,对于锁存在哪个位置,我们已经清楚了,锁存在于每个对象的 markOop 对象头中.对于为什么每个对象都可以成为锁呢? 因为每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,而对应的 oop/oopDesc 都会存在一个markOop 对象头,而这个对象头是存储锁的位置,里面还有对象监视器,即ObjectMonitor,所以这也是为什么每个对象都能成为锁的原因之一。那么 synchronized是如何实现锁的呢?
三、synchronized是如何实现锁:
了解了对象头以及monitor以后,接下来去分析synchronized的锁的实现,就会相对简单了。前面讲过synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁.锁的类型:锁从宏观上分类,分为悲观锁与乐观锁。
乐观锁:
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
自旋锁(CAS):
自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制;
偏向锁:
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。下图就是偏向锁的获得跟撤销流程图:

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。执行同步块。这个时候线程2也来访问同步块,也是会检查对象头的Mark Word里是否存储着当前线程2的偏向锁,发现不是,那么他会进入 CAS 替换,但是此时会替换失败,因为此时线程1已经替换了。替换失败则会进入撤销偏向锁,首先会去暂停拥有了偏向锁的线程1,进入无锁状态(01).偏向锁存在竞争的情况下就回去升级成轻量级锁。
开启:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx1024m -Xms1024m
关闭:-XX:+UseBiasedLocking -client -Xmx512m -Xms512m
轻量级锁:
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,下面是轻量级锁的流程图:

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这个时候 JVM会尝试使用 CAS 将 mark Word 更新为指向栈帧中的锁记录(Lock Record)的空间指针。并且把锁标志位设置为 00(轻量级锁标志),与此同时如果有另外一个线程2也来进行 CAS 修改 Mark Word,那么将会失败,因为线程1已经获取到该锁,然后线程2将会进行 CAS操作不断的去尝试获取锁,这个时候将会引起锁膨胀,就会升级为重量级锁,设置标志位为 10.
由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程进入等待。
重量级锁:
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。
monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现 monitor。他的锁的获取过程的体现会简单很多。每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。

这里提到的 CXQ跟 EnterList 是什么呢? 见下图:

这里我们重新回到 objectMonitor.cpp 这个源码中来看以下:
void ATTR ObjectMonitor::enter(TRAPS) {//获取重量级锁的过程
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;//进行CAS自旋操作
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//自旋结果相等,则重入(重入的原理)
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//接下去就是有并发的情况下竞争的过程了
....
所以这就是synchronized实现锁的一个过程。
wait和notify的原理:
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会通知等待线程可以醒了,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

看一下 JVM 源码中的逻辑,在objectMonitor.cpp 中:在我们Java代码层面调用的 wait() 方法后,其实在 JVM 层面所作的是,封装 ObjectWaiter 对象并将其放入 _WaitSet 队列,并调用 park()将线程挂起。
// Wait/Notify/NotifyAll
// Note: a subset of changes to ObjectMonitor::wait()
// will need to be replicated in complete_exit above
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
Thread * const Self = THREAD ;
assert(Self->is_Java_thread(), "Must be Java thread!");
JavaThread *jt = (JavaThread *)THREAD;
DeferredInitialize () ;
// Throw IMSX or IEX.
CHECK_OWNER();//检查objectMonitor对象是否指向本线程(即是否获得锁)
// ... 省略中间的代码
// create a node to be put into the queue
// Critically, after we reset() the event but prior to park(), we must check
// for a pending interrupt.
// 封装了一个ObjectWaiter对象
ObjectWaiter node(Self);
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
OrderAccess::fence();//内存屏障 // ST into Event; membar ; LD interrupted-flag
// Enter the waiting queue, which is a circular doubly linked list in this case
// but it could be a priority queue or any data structure.
// _WaitSetLock protects the wait queue. Normally the wait queue is accessed only
// by the the owner of the monitor *except* in the case where park()
// returns because of a timeout of interrupt. Contention is exceptionally rare
// so we use a simple spin-lock instead of a heavier-weight blocking lock.
//将ObjectWaiter放入 _WaitSet中
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
AddWaiter (&node) ;
Thread::SpinRelease (&_WaitSetLock) ;
if ((SyncFlags & 4) == 0) {
_Responsible = NULL ;
}
intptr_t save = _recursions; // record the old recursion count
_waiters++; // increment the number of waiters
_recursions = 0; // set the recursion level to be 1
exit (true, Self) ; // exit the monitor
guarantee (_owner != Self, "invariant") ;
//.....省略中间代码
// The thread is on the WaitSet list - now park() it.
// On MP systems it's conceivable that a brief spin before we park
// could be profitable.
// TODO-FIXME: change the following logic to a loop of the form
// while (!timeout && !interrupted && _notified == 0) park()
int ret = OS_OK ;
int WasNotified = 0 ;
{ // State transition wrappers
OSThread* osthread = Self->osthread();
OSThreadWaitState osts(osthread, true);
{
ThreadBlockInVM tbivm(jt);
// Thread is in thread_blocked state and oop access is unsafe.
jt->set_suspend_equivalent();
if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
// Intentionally empty
} else
if (node._notified == 0) {
if (millis <= 0) { // 调用park()将线程挂起
Self->_ParkEvent->park () ;
} else {
ret = Self->_ParkEvent->park (millis) ;
}
}
.......
}
接下去看看 notify 的操作:
void ObjectMonitor::notify(TRAPS) {
CHECK_OWNER();//同样先检查objectMonitor对象是否指向本线程
if (_WaitSet == NULL) {//判断wait队列是否为空
TEVENT (Empty-Notify) ;
return ;
}
DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
int Policy = Knob_MoveNotifyee ;
// 这个 WaitSet - notify 很了然
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
ObjectWaiter * iterator = DequeueWaiter() ;//dequeue _WaitSet 队列
if (iterator != NULL) {//不为空,然后接下去就是一系列的判断,最后去唤醒
//.......
}
}
wait和notify为什么需要在synchronized里面:
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。
轻量级锁
大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。
互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。
为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。
轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。
本文将详细介绍JVM如何利用CAS,实现轻量级锁。
原理详解
Java Object Model中定义,Object Header是一个2字(1 word = 4 byte)长度的存储区域。
第一个字长度的区域用来标记同步,GC以及hash code等,官方称之为 mark word。第二个字长度的区域是指向到对象的Class。
在2个word中,mark word是轻量级锁实现的关键。它的结构见下表

从表中可以看到,state为lightweight locked的那行即为轻量级锁标记。bitfieds名为指向lock record的指针,这里的lock record,其实是一块分配在线程堆栈上的空间区域。
用于CAS前,拷贝object上的mark word(为什么要拷贝,请看下文)。
第三项是重量级锁标记。后面的状态单词很有趣,inflated,译为膨胀,在这里意思其实是锁已升级到OS-level。
在本文的范围内,我们只关注第二和第三项即可。
为了能直观的理解lock,unlock与mark word之间的联系,我画了一张流程图:

在图中,提到了拷贝object mark word,由于脱离了原始mark word,官方将它冠以displaced前缀,即displaced mark word(置换标记字)。
这个displaced mark word是整个轻量级锁实现的关键,在CAS中的compare就需要用它作为条件。
为什么要拷贝mark word?
其实很简单,原因是为了不想在lock与unlock这种底层操作上再加同步。
在拷贝完object mark word之后,JVM做了一步交换指针的操作,即流程中第一个橙色矩形框内容所述。
将object mark word里的轻量级锁指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用。
lock record里的owner指针指向object mark word的作用是为了在接下里的运行过程中,识别哪个对象被锁住了。
下图直观地描述了交换指针的操作。


最后一步unlock中,我们发现,JVM同样使用了CAS来验证object mark word在持有锁到释放锁之间,有无被其他线程访问。
如果其他线程在持有锁这段时间里,尝试获取过锁,则可能自身被挂起,而mark word的重量级锁指针也会被相应修改。
此时,unlock后就需要唤醒被挂起的线程。
偏向锁
阅读本文的读者,需要对Java轻量级锁有一定的了解,知道lock record, mark word之类的名词。
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
轻量级锁也是一种多线程优化,它与偏向锁的区别在于,轻量级锁是通过CAS来避免进入开销较大的互斥操作,而偏向锁是在无竞争场景下完全消除同步,连CAS也不执行(CAS本身仍旧是一种操作系统同步原语,始终要在JVM与OS之间来回,有一定的开销)。
所谓的无竞争场景,举个例子,就是单线程访问带同步的资源或方法。
偏向锁实现原理
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)
通过下图可以更直观的理解偏向锁:

这张图,省略了轻量级锁相关的几处步骤,将关注点更多地聚焦在偏向锁的状态变化上。
偏向模式和非偏向模式,在下面的mark word表中,主要体现在thread ID字段是否为空。

挂起持有偏向锁的线程,这步操作类似GC的pause,但不同之处是,它只挂起持有偏向锁的线程(非当前线程)。
在抢占模式的橙色区域说明中有提到,指向当前堆栈中最近的一个lock record(在轻量级锁原理一文有讲到,lock record是进入锁前会在stack上创建的一份内存空间)。
这里提到的最近的一个lock record,其实就是当前锁所在的stack frame上分配的lock record。
整个步骤是从偏向锁恢复到轻量级锁的过程。
偏向锁也会带来额外开销
在JDK6中,偏向锁是默认启用的。它提高了单线程访问同步资源的性能。
但试想一下,如果你的同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。事实上,消除偏向锁的开销还是蛮大的。
所以在你非常熟悉自己的代码前提下,大可禁用偏向锁 -XX:-UseBiasedLocking 。


浙公网安备 33010602011771号