synchronized
是Java提供的一种内置锁,通常叫做重量级锁
。在Java SE 1.6对其进行了各种优化。
利用synchronized
实现同步的基础:Java中的每个对象都可以作为锁。具体表现为以下形式:
// ①普通同步方法,锁的是当前实例对象。
public synchronized void instanceLock() {
// code
}
// ②静态同步方法,锁的是当前的Class对象。
public static synchronized void classLock() {
// code
}
// ③同步方法块,锁是synchronized括号内配置的对象
final Object lock = new Object();
public void blockLock() {
synchronized (lock) {
// code
}
}
// ④等同于①,锁的是当前实例对象。
public void instanceLock2() {
synchronized (this) {
// code
}
}
// ⑤等同于②,锁的是当前的Class对象。
public void classLock2() {
synchronized (this.getClass()) {
// code
}
}
当一个线程试图访问同步块时,必须先获得锁,正常退出或抛出异常时须释放锁。
JVM基于进入和退出Monitor对象来实现方法同步和代码块的同步。代码块同步使用monitorenter和monitorexit指令实现的,方法的同步使用ACC_SYNCHRONIZED标识。
monitorenter是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何一个对象都有一个monitor与之关联,当monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取锁。
synchronized用的锁是存在Java对象头中的。若果对象是数组类型,则虚拟机使用3个字宽(Word)存储对象头,如果对象是非数组类型,则使用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。
Java对象头长度
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的HashCode、分代年龄和锁标识位 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组长度(如果是数组) |
Mark Word格式
锁状态 | 29bit / 61bit | 1bit是否偏向锁 | 2bit锁标志位 |
---|---|---|---|
无锁 | 0 | 01 | |
偏向锁 | 线程ID | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 该位不用于标识偏向锁 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 该位不用于标识偏向锁 | 10 |
GC标记 | 该位不用于标识偏向锁 | 11 |
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java 1.6中,锁一共有4种状态:无锁状态 > 偏向锁状态 > 轻量级锁状态 > 重量级锁状态。
无锁就是没有对资源进行锁定,任何线程都可以尝试修改它。
几种锁会随着竞争情况逐渐升级,锁的升级很容易,但是锁降级发送的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点时,会检查是否有闲置的锁,然后进行降级。
HotSpot作者经研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低而引入了偏向锁。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
当一个线程第一次访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存放偏向的线程ID。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
若测试成功,则表示线程已经获得了锁;若测试失败,则表示有另外一个线程来竞争这个偏向锁。此时会尝试使用CAS来替换Mark Word里面的线程ID为新线程ID,这时有两种情况:
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识。大概过程如下:
如果程序里的锁常处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking
,那么程序默认会进入轻量级锁状态。
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
自旋是消耗CPU的,一直无法获取锁则一直处于自旋状态。JDK采用了适应性自旋,简单来说就是线程如果自旋成功则下次自旋次数增加,若失败则下次自旋次数会减少。
自旋并非一直自旋下去,如果自旋到一定程度(和JVM、OS相关),依旧没获取到锁,称自旋失败,那么线程会阻塞。同时锁将会升级成重量级锁。
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word内容复制回锁对象Mark Word里面。如果没有发生竞争,那么这个复制操作会成功。若有其他线程因为自旋多次导致轻量级锁升级成重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
重量级锁依赖于操作系统的互斥量(mutex)实现的,而操作系统中线程中间状态的转换需要相对比较长的时间,所以重量级锁效率比较低,但被阻塞的线程不会消耗CPU。
每个对象都可以当做一个锁,当多个线程同时请求某个锁对象时,对象锁会设置几种状态来区分请求的线程:
名称 | 描述 |
---|---|
Contention List | 所有请求锁的线程将被首先放置到该竞争队列 |
Entry List | Contention List中那些有资格成为候选人的线程被移到Entry List |
Wait Set | 那些调用wait方法被阻塞的线程将会被放到Wait Set |
OnDeck | 任何时刻最多只能有一个线程正在竞争锁,该线程成为OnDeck |
Owner | 获得锁的线程 |
!Owner | 释放锁的线程 |
当一个线程尝试获取锁时,如果该锁已经被占用,则会将线程封装成一个ObjectWaiter对象插入到Contention List的队列的队首,然后调用park函数挂起当前线程。
如果线程获得锁后,调用Object.wait方法,则将线程加入到Wait Set中,当被Object.notify唤醒后,会将线程从Wait Set移动到Contention List或Entry List中去。需注意的是,当调用一个锁对象的wait或notify方法时,如果当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级所 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不会适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块的执行时间较长 |
每个线程在准备获取共享资源时:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有