Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发-synchronized

Java并发-synchronized

作者头像
lpe234
发布于 2021-03-02 07:33:50
发布于 2021-03-02 07:33:50
41400
代码可运行
举报
文章被收录于专栏:若是烟花若是烟花
运行总次数:0
代码可运行

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

1 基本使用及原理

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ①普通同步方法,锁的是当前实例对象。
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
并发编程锁之synchronized(二)
并发编程中数据同步需要依赖锁进行控制,上篇博文通过ReentrantLock源码分析也对Lock实现锁机制的大致原理有了一个了解,Lock主要是通过编码的方式实现锁,其核心就是:CAS+循环,CAS原子操作需要依赖底层硬件层特殊的CPU指令。这节我们来看下Java中另一种非常常见的实现同步的方式:synchronized。synchronized主要通过底层JVM进行实现,而且JVM为了优化,产生偏向锁、轻量级锁、重量级锁,由于其处于JVM底层实现中,对很多并发编程人员来说能清晰理解它们间的区别还是件困难的事。通过本篇博文,构建出对Java中锁得体系结构,让你对其有个更系统全面的认知。
技术zhai
2019/02/15
4440
并发编程学习笔记02-Java并发机制的底层原理之synchronized
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为三种形式:
WindCoder
2020/01/22
3270
【死磕Java并发】—–深入分析synchronized的实现原理
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线程情况的百试不爽的良药。但是,随着我们学习的进行我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。 诚然,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。下面跟随LZ一
芋道源码
2018/03/02
8090
【死磕Java并发】—–深入分析synchronized的实现原理
synchronized详解
在Java中,synchronized锁可能是我们最早接触的锁了,在 JDK1.5之前synchronized是一个重量级锁,相对于juc包中的Lock,synchronized显得比较笨重。
三分恶
2021/03/03
6240
52.说一下 synchronized 底层实现原理?_synchronized底层实现
说起多线程同步,一般的方案就是加锁,而在 java 中,提到加锁就想起 juc 包提供的 Lock 接口实现类与默认的关键字 synchronized 。我们常听到,juc 下的锁大多基于 AQS,而 AQS 的锁机制基于 CAS,相比起 CAS 使用的自旋锁,Synchronized 是一种重量级的锁实现。
全栈程序员站长
2022/09/23
2K1
52.说一下 synchronized 底层实现原理?_synchronized底层实现
synchronized
Synchronized是同步中的鼻祖,很多人叫他重量级锁,也是最基本的同步互斥手段。随着Java版本不断提高,尤其是在Java6之后Synchronized进行了很多性能优化。本章首先要简单介绍对象头的内容,然后引申出Synchronized的实现原理,锁的储存结构和锁升级等,以及相关所有锁的概念,都会一一向大家介绍。
胖虎
2020/12/08
4980
浅析 synchronized 底层实现与锁相关 | Java
synchronizrd 是开发中解决同步问题中最常见,也是最简单的一种方法。从最开始学习并发编程,我们都知道,只要加上这个 synchronizrd 关键字,就可以很大程度上轻松解决同步问题。相应的,从原理上来讲,其也是比较重的一种操作,特别是 jdk1.5 时候,相比 JUC 中的 Lock 锁,一定程度上逊色不少。但随着jdk1.6对 synchronized 的优化后,synchronizrd 并不会显得那么重,相比使用 Lock 而言,其的性能大多数情况下也可以接近 Lock 。
Petterp
2022/02/09
3460
浅析 synchronized 底层实现与锁相关 | Java
synchronized—深入总结
传统的锁(也就是下文要说的重量级锁)依赖于系统的同步函数,在linux上使用mutex互斥锁,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了synchronized关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的。
用户5325874
2020/01/16
5940
synchronized—深入总结
【JAVA 进阶之锁机制】synchronized 的锁升级-持续更新....
1、synchronized 的基本认识 场景:Synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全,而Synchronized关键字就是用于代码同步。什么情况下会数据不安全呢,要满足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。 在多线程并发编程中 synchronized 称呼为重量级锁。但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。 1.1 synchronized 有三种方式来加锁 1.1.1. 修饰实例方法,作用于当前实例加锁,进入同步代码前 要获得当前实例的锁 1.1.2. 静态方法,作用于当前类对象加锁,进入同步代码前要 获得当前类对象的锁 1.1.3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同 步代码库前要获得给定对象的锁。 1.2 Mark word Mark word 记录了对象和锁有关的信息,当某个对象被 synchronized 关键字当成同步锁时,那么围绕这个锁的一 系列操作都和 Mark word 有关系。Mark Word 在 32 位虚 拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。 Mark Word 里面存储的数据会随着锁标志位的变化而变化, Mark Word 可能变化为存储以下 5 中情况
用户5640963
2021/05/27
8380
Java并发编程之synchronized底层原理
对象头包含两部分:运行时元数据(Mark Word)和类型指针 (Klass Word)
冬天vs不冷
2025/01/21
1200
Java并发编程之synchronized底层原理
并发基础之Synchronized原理
上篇文章和大家聊了聊hashmap和concurrenthashmap的结构、用法、原理,从这篇文章开始次我们来聊聊并发编程吧!本次我将带大家了解一下synchronized的原理。
崩天的勾玉
2021/12/20
2880
并发基础之Synchronized原理
Java并发编程:synchronized和锁优化
1. 使用方法 synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面: 确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区 保证共享变量的修改能及时可见 有效解决重排序问题 语义上来讲,synchronized主要有三种用法: 修饰普通方法,锁的是当前对象实例(this) 修饰静态方法,锁的是当前 Class 对象(静态方法是属于类,而不是对象) 修饰代码块,锁的是括号里的对象 2. 实现原理 2.1. 监视器锁 synchroniz
butterfly100
2018/04/16
8870
Java并发编程:synchronized和锁优化
Synchronized深入分析
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
Anymarvel
2020/09/23
6870
Java程序员必知的并发编程艺术——并发机制的底层原理实现
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
美的让人心动
2018/09/20
4490
Java程序员必知的并发编程艺术——并发机制的底层原理实现
Java基础之Synchronized原理
思维导图svg: https://note.youdao.com/ynoteshare1/index.html?id=eb05fdceddd07759b8b82c5b9094021a&type=no
Ryan-Miao
2020/07/01
4710
java并发编程实战(2) 线程同步synchronized
synchronized保证语句块内操作是原子的,所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断。synchronized和volatile最大的区别就在于原子性,volatile不具备原子性。
黄规速
2022/04/14
4700
java并发编程实战(2) 线程同步synchronized
彻底理解Java并发:synchronized关键字
Synchronize 翻译成中文:同步,使同步。synchronized:已同步。synchronized 能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。也就是说 Synchronized 在某个线程将资源锁住了之后,其他线程只有在当前线程使用完成后,才可以接着使用。
栗筝i
2022/12/01
6020
彻底理解Java并发:synchronized关键字
超硬核Synchionized底层实现原理
synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。
名字是乱打的
2021/12/22
3050
超硬核Synchionized底层实现原理
Java并发机制的底层实现原理 - synchronized和volatile
synchronized和volatile几乎是java面试基础部分必会,不会你就吃亏了,这一篇文章摘抄《Java并发编程的艺术》
王炸
2019/07/02
5360
Java并发机制的底层实现原理 -  synchronized和volatile
Java程序员必知的并发编程艺术——并发机制的底层原理实现
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。 volatile借助Java内存模型保证所有线程能够看到最新的值。(内存可见性) 实现原理: 将带有volatile变量操作的Java代码转换成汇编代码后,可以看到多了个lock前缀指令(X86平台CPU指令)。这个lock指令是关键,在多核处理器下实现两个重要操作: 1.将当前处理器缓存行的数据写回到系统内存。 2.这个写回内存的操作会使其他处理器里缓存该内存地址的数据失效 如果了解
Java架构
2018/05/04
7700
Java程序员必知的并发编程艺术——并发机制的底层原理实现
相关推荐
并发编程锁之synchronized(二)
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验