前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发-synchronized

Java并发-synchronized

作者头像
lpe234
发布2021-03-02 15:33:50
3810
发布2021-03-02 15:33:50
举报
文章被收录于专栏:若是烟花若是烟花

synchronized是Java提供的一种内置锁,通常叫做重量级锁。在Java SE 1.6对其进行了各种优化。

1 基本使用及原理

利用synchronized实现同步的基础:Java中的每个对象都可以作为锁。具体表现为以下形式:

代码语言:javascript
复制
// ①普通同步方法,锁的是当前实例对象。
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所有权,即尝试获取锁。

2 Java对象头

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

3 锁升级降级

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java 1.6中,锁一共有4种状态:无锁状态 > 偏向锁状态 > 轻量级锁状态 > 重量级锁状态。

无锁就是没有对资源进行锁定,任何线程都可以尝试修改它。

几种锁会随着竞争情况逐渐升级,锁的升级很容易,但是锁降级发送的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点时,会检查是否有闲置的锁,然后进行降级。

3.1 偏向锁

HotSpot作者经研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低而引入了偏向锁。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。

3.1.1 实现原理

当一个线程第一次访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存放偏向的线程ID。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

若测试成功,则表示线程已经获得了锁;若测试失败,则表示有另外一个线程来竞争这个偏向锁。此时会尝试使用CAS来替换Mark Word里面的线程ID为新线程ID,这时有两种情况:

  • 成功,表示之前线程不存在了,Mark Word里面的线程ID为新线程ID,锁不会升级,仍为偏向锁。
  • 失败,表示之前的线程仍然存在,那么会暂停之前的线程,设置偏向锁标识为0,并设置锁标识位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
3.1. 偏向锁撤销及关闭

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识。大概过程如下:

  • 在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
  • 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其变成无锁状态。
  • 唤醒被停止的线程,将当前锁升级为轻量级锁。

如果程序里的锁常处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking,那么程序默认会进入轻量级锁状态。

3.2 轻量级锁

多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。

3.2.1 轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

自旋是消耗CPU的,一直无法获取锁则一直处于自旋状态。JDK采用了适应性自旋,简单来说就是线程如果自旋成功则下次自旋次数增加,若失败则下次自旋次数会减少。

自旋并非一直自旋下去,如果自旋到一定程度(和JVM、OS相关),依旧没获取到锁,称自旋失败,那么线程会阻塞。同时锁将会升级成重量级锁。

3.2.2 轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word内容复制回锁对象Mark Word里面。如果没有发生竞争,那么这个复制操作会成功。若有其他线程因为自旋多次导致轻量级锁升级成重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

3.3 重量级锁

重量级锁依赖于操作系统的互斥量(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方法时,如果当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

4 锁的优缺点对比

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步块场景

轻量级所

竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程,使用自旋会消耗CPU

追求响应时间,同步块执行速度非常快

重量级锁

线程竞争不会适用自旋,不会消耗CPU

线程阻塞,响应时间缓慢

追求吞吐量,同步块的执行时间较长

5 总结锁升级流程

每个线程在准备获取共享资源时:

  • 第一步,检查MarkWord里面存放的是不是自己的ThreadId,若是则表示当前处于“偏向锁”。
  • 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候使用CAS来执行进行切换,新线程根据Mark Word里面现有的ThreadId,通知之前的线程暂停,之前的线程将MarkWord置空。
  • 第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把锁对象的MarkWord的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
  • 第四步,第三步中成功执行CAS的获得资源,失败则进入自旋。
  • 第五步,自旋的线程在自旋的过程中,成功获得资源(即之前获得资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁状态,如果自旋失败。
  • 第六步,进入重量级锁状态,这个时候,自旋的线程进行阻塞,等待之前的线程执行完成并唤醒自己。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 基本使用及原理
  • 2 Java对象头
  • 3 锁升级降级
  • 3.1 偏向锁
    • 3.1.1 实现原理
      • 3.1. 偏向锁撤销及关闭
      • 3.2 轻量级锁
        • 3.2.1 轻量级锁加锁
          • 3.2.2 轻量级锁解锁
          • 3.3 重量级锁
          • 4 锁的优缺点对比
          • 5 总结锁升级流程
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档