资料总结 资料总结
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

linghui Wu

一只努力学飞的鱼
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • java-se

    • java集合
    • 计算机基础
    • 并发编程
    • java线程
    • java协程
    • synchronized
      • 如何解决线程并发安全问题?
      • 多线程执行的过程是不可控的?
        • synchronized的使用
      • 加锁方式
      • sychronized的唤醒策略
        • 底层原理
      • 字节码
      • Monitor(管程/监视器)
        • 什么是monitor
        • Monitor监视器锁
      • Java内置管程synchronized
      • Monitor机制在Java中的实现
        • wait()的正确使用姿势
        • notify()和notifyAll()分别何时使用
        • 对象的内存布局
      • 对象头
        • Mark Word
        • Klass Pointer
        • 数组长度
      • 实例数据
      • 对齐填充
      • 对象头分析工具
        • 锁的膨胀升级
      • 膨胀升级过程
      • 无锁状态
      • 偏向锁
        • 偏向锁延迟偏向
      • 轻量级锁
      • 重量级锁(自旋锁)
      • 自旋优化
      • 锁粗化
      • 锁消除
        • 逃逸分析
      • synchronized和lock的选择
        • 参考资料
    • Unsafe&Atomic
    • AQS
    • Lock
    • JUC工具杂记
    • Queue
    • 线程池原理
    • Future
    • ForkJoin
    • BIO,NIO,AIO
  • jvm

  • mybatis

  • Netty

  • 爬虫 webmagic

  • spring

  • spring-cloud

  • 中间件

  • flowable

  • idea工具

  • maven

  • ms

  • java部署

  • 原生安卓

  • java
  • java-se
wulinghui
2022-02-13
目录

synchronized

# 设计同步器的意义

# 如何解决线程并发安全问题?

  • 序列化访问临界资源::即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
  • 多线程共享,可变

# 多线程执行的过程是不可控的?

所以需要采用同步机制来协同对对象可变状态的访问!

# synchronized的使用

synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。

# 加锁方式

synchronized 实际是用对象锁保证了临界区内代码的原子性

  • 实例方法
  • 静态方法
  • 代码块

# sychronized的唤醒策略 (opens new window)

  • 如果没有其它线程等锁,结束;
  • 优先唤醒 EntryList 链表头部的线程;
  • 如果 EntryList 是空的,把 cxq 链表赋值给 EntryList 链表,再唤醒 EntryList 头部的线程。

而由于入队的时候 cxq 链表是头插的,所以 synchronized 默认的唤醒策略是,最后阻塞等锁的最先唤醒。

# 底层原理

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换(简称:上下文切换),对性能有较大影响。所以它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平有时更快。

# 字节码

Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和 MonitorExit指令来实现。

# Monitor(管程/监视器)

Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

# 什么是monitor

  • 一个同步工具、一种同步机制、描述为一个对象
  • 每一个Java对象本身就带一把 看不见的锁,它叫做内部锁或者Monitor锁
  • Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式 获取锁的

# Monitor监视器锁

Synchronized的语义底层是通过一个monitor的对象来 完成,其实wait/notify等方法也依赖于monitor对象。这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则 会抛出java.lang.IllegalMonitorStateException的异常的原因

# Java内置管程synchronized

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。就是wait/notify/notifyAll。这样做简化了使用门槛。

# Monitor机制在Java中的实现 (opens new window)

具体得看jvm源码了。。这篇 https://www.cnblogs.com/qingshan-tang/p/12698705.html 博客写的还不错。

# wait()的正确使用姿势

对于MESA管程来说,有一个编程范式: 再wait方法中的demo就是这样写的.

     *     synchronized (obj) {
     *         while (<condition does not hold>)
     *             obj.wait();
     *         ... // Perform action appropriate to condition
     *     }
1
2
3
4
5

为什么要这样写呢?? 唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。

# notify()和notifyAll()分别何时使用

满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需要唤醒一个线程。
  • 对象的内存布局

    • 对象头

      • hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度等等

      • Java对象头一般占有2个机器码,如果对象是数组类型,则需要3个机器码

      • Mark Word

        • 存储对象自身的运行时数据
        • Mark Word会随着程序的运行发生变化,以复用自己的存储空间
        • 哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
      • 哪些信息会被指针压缩

        • 对象的全局静态变量(即类属性)
        • 对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
        • 对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
        • 对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

# 对象的内存布局

# 对象头

  • hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度

  • Java对象头一般占有2个机器码,如果对象是数组类型,则需要3个机器码

# Mark Word

  • 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。
  • Mark Word会随着程序的运行发生变化,以复用自己的存储空间

# Klass Pointer

对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。

哪些信息会被指针压缩:

  • 对象的全局静态变量(即类属性)
  • 对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
  • 对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
  • 对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

# 数组长度

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

# 实例数据

存放类的属性数据信息,包括父类的属性信息;

# 对齐填充

由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

# 对象头分析工具

运行时对象头锁状态分析工具JOL

# 锁的膨胀升级

# 膨胀升级过程

  • 无锁状态、偏向锁、轻量级锁和重量级锁。只能从低到高升级,不会锁降级
  • JDK 1.6 中默认是开启偏向锁和轻量级锁的

# 无锁状态

# 偏向锁

线程获得了锁,那么锁就进入偏向模式,当同一线程再次请求锁时,无需再做任何同步操作,省去大量的锁申请操作,从而也就提高性能

# 偏向锁延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。

# 轻量级锁

  • 转化时机:::修改偏向锁线程ID失败后,撤销偏向锁标识,改成轻量级锁的结构
  • 应用场景:::: 对绝大部分的锁,在整个同步周期内都不存在竞争,也就是几个线程交替执行同步块的场合。

# 重量级锁(自旋锁)

  • 适应性自旋
  • Mutex互斥量 (涉及上下文切换)

# 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

# 锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。 比如: 每次调用 StringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

# 锁消除

  • JIT编译时,去除不可能存在共享资源竞争的锁,节省毫无意义的请求锁时间

  • 如: StringBuffer的append是一个同步方法,但是在add方法中StringBuffer属于一个局部变量,并且不会被其他线程所使用,JVM会自动将其锁消除

  • 锁消除的依据是逃逸分析的数据支持

    • java必须运行在server模式 -server
    • 同时必须开启逃逸分析 -XX:+DoEscapeAnalysis
    • 开启锁消除 -XX:+EliminateLocks

# 逃逸分析

从jdk 1.7开始已经默认开启逃逸分析 优化

  • 同步省略
  • 将堆分配转化为栈分配
  • 分离对象或标量替换

# synchronized和lock的选择

参考 [并发编程]synchronized与lock的性能比较 (opens new window)

性能:在高并发的情况下都是在一个数量集的,较高和较低并发下,synchronized都比Lock来的快,基本是两倍的关系。

我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化,可以考虑使用lock。

这里和AQS原理相关: LockSupport.park 也是操作系统的mutex和重量级锁一样了。 所以低并发下,synchronized是偏向锁和轻量级锁。 超高并发下: AQS的cas空循环再

# 参考资料

Unsafe.park和Unsafe.unpark的底层实现原理 (opens new window)

https://cloud.tencent.com/developer/article/1460321 在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。

编辑 (opens new window)
上次更新: 2023/01/24, 15:21:15
java协程
Unsafe&Atomic

← java协程 Unsafe&Atomic→

最近更新
01
架构升级踩坑之路
02-27
02
总结
02-27
03
语法学习
02-27
更多文章>
| Copyright © 2021-2025 Wu lingui |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式