type
status
date
slug
summary
tags
category
icon
password
这里写文章的前言:
一个简单的开头,简述这篇文章讨论的问题、目标、人物、背景是什么?并简述你给出的答案。
可以说说你的故事:阻碍、努力、结果成果,意外与转折。
📝 synchronized
前置条件
临界区
- 一个程序运行多线程本身没有问题
- 多个线程访问共享资源
- 多个线程访问共享资源也没问题
- 多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
竞态条件
多个线程在临界区内执行,由于代码顺序序列不同而导致结果无法预测,称之为发生了竟态条件
- 阻塞解决方法: Synchronized,Lock
- 非阻塞解决方法: 原子变量
synchronized
synchronized 同步快是 java 提供的一种原子性内置锁,java中的对个对象都可以当作一个同步锁来使用,这些java内置的使用者看不到等锁被称为内置锁,也叫做监视器 基于 Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量), 它是一个重量级锁,性能较低。 1.5之后进行了优化,锁粗化,锁消除,轻量级锁,偏向锁,自适应自旋等技术来减少锁操作的开销 同步方法是通过 access_flags 中设置 ACC_SYNCHRONIZED 标志来实现;同步代码快是通过 monitorenter 和 monitorexit来实现。两个指令等的执行是JVM通过调用操作系统的斥原语mutex来实现,被阻塞的线程会被挂起来,等待重新调度,会导致 “用户态和内核态” 两个态之间来回切换,对性能影响较大
原子性
synchronized 保证同一时间只有一个线程拿到锁,能够进入同步代码快
可见性
执行 synchronized 时,会对应lock原子操作回刷新共享变量的值
有序性
as-if-serial: 不管编译器和CPU如何重拍序,必须保证在单线程情况下程序结果是正确的,还有就是数据依赖的也是不能重拍序的
可重入锁
synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了
不可中断
一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个会一直阻塞或者等待,不可被中断
Monitor(管程/监视器)
管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发
MESA模型
唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait方法有一个超时参数,为了避免线程进入等待队列永久阻塞

notify和notifyAll
满足以下三个条件时,可以使用notify,其余情况尽量使用notifyall
- 所有等待线程拥有相同的等待条件
- 所有等待线程被唤醒后,执行相同的操作
- 只需要唤醒一个线程
Monitor机制java实现
ObjectMonitor 数据结构如下:
对象内存布局
对象在内存中存储等布局可以分为三块区域: 对象头(Header),实例数据(Instance Data)和对齐填充(Padding)
- 对象头:hash码,对象所属年代,锁状态标志,偏向锁(线程),偏向时间,数组长度(数组对象才有)
- 实例数据:存放类的属性数据信息,包括父类的属性信息
- 对齐填充:由于虚拟机要求对象起始地址必须是8字节的倍数,填充数据不是必须存在等,仅仅是为了字节对齐

对象头详解
- Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”
- Klass Pointer:对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
- 数组长度:如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度
用户态和内核态
我们所有的程序都是用户空间运行,进入用户运行状态也是(用户态),但是很多操作可能涉及内核运行,比如IO,就会进入内核运行状态(内核态)
- 用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供的服务
- 用户态执行系统调用
- CPU切换到内核态,跳转到对应的内存指定的位置执行指令
- 系统调用处理器去读取我们先前放到内存的数据参数,并执行下个指令
- 调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令
锁升级场景
32位JVM对象结构

64位JVM 对象结构

锁标识位置

锁状态变化

偏向锁批量偏向&批量撤销
从偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入一个同步快时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获取锁时,就需要等于safe point时,再将偏向锁撤销为无锁状态或者升级为轻量锁,会消耗一定的性能。所以在多线程的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是就有了批量偏向锁与批量撤销的机制
锁粗化
假设一系列的连续操作都会对同一个对象反复加锁以及解锁,甚至加锁操作是出现在循环中的,即使没有出现线程的竞争,频繁的进行互斥同步操作也会导致不必要等性能损耗。如果JVM检测到有一连串零碎的操作都是同一个对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列等外部
锁消除
锁消除即删除不必要的加锁操作。锁消除是在java虚拟机在JIT编译期间,通过运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间
逃逸分析
- 逃逸分析,是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,java hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域。
- 方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中
- 线程逃避:这个对象甚至可能被其他线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量
使用逃逸分析,编译器可以对代码优化:
- 同步省略或者锁消除。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作是不考虑同步的
- 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 分离对象 或 标量替换。有的对象可能不需要连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
🤗 总结归纳
📎 参考文章
有关文章的问题,欢迎您在底部评论区留言,一起交流~