首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
1.1 synchronized 简述
synchronized 被称为重量级锁 , 但是 1.6 版本后得到了优化 , 相对轻量了很多, 它可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块 .
- 主要操作对象是方法或者代码块中存在的共享数据, 同时可保证一个线程的变化(主要是共享数据的变化)被其他线程所看
- synchronized 的核心原理为 Java 对象头 以及 Monitor
- JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
1.2 Java 对象头 和 Monitor
对象头结构
1 | java复制代码// 原理 --> |
32 位虚拟机 Mark Word >>>>
64 位虚拟机 Mark Word >>>>
数据结构
1 | java复制代码// Monitor 的实现方式 @ https://blog.csdn.net/javazejian/article/details/70768369 |
- 1 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合, 此时开始尝试获取monitor
- 2 当线程获取到对象的monitor 后进入 _Owner 区域 ,并把 monitor中的owner变量 设置为当前线程同时monitor中的计数器count加1
- 3 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。
- 4 若当前线程执行完毕 也将 释放monitor(锁) 并 复位变量的值,以便 其他线程进入获取monitor(锁)
Monitor 指令
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配
查看汇编情况 :
1 | java复制代码// Step 1 : 准备简单的Demo |
处理逻辑详情 :
1 | java复制代码// synchronized 源码分析 @ https://blog.csdn.net/javazejian/article/details/70768369 |
synchronized 方法底层逻辑 (ACC_SYNCHRONIZED标识)
- 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
- JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
- 方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置
|- 如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。 - 在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor
异常处理 : 如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放
synchronized 内存级原理
1 | java复制代码// 最后生成的汇编语言 |
- synchronized的底层操作含义是先对对象头的锁标志位用lock cmpxchg的方式设置成”锁住”状态
- 释放锁时,在用lock cmpxchg的方式修改对象头的锁标志位为”释放”状态,写操作都立刻写回主内存。
- JVM会进一步对synchronized时CAS失败的那些线程进行阻塞操作,这部分的逻辑没有体现在lock cmpxchg指令上,我猜想是通过某种信号量来实现的。
- lock cmpxchg 指令前者保证了可见性和防止重排序,后者保证了操作的原子性。
1.3 synchronized 用法
1 | java复制代码// 加锁方式 ,当前实例 ,当前class , 自定义object |
synchronized关键字最主要的三种使用方式:
- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
- 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。
- 所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。
- synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。
另外需要注意的是 尽量不要使用 synchronized(String a), 部分字符串常量会缓冲到常量池里面, 不过可以试试 new String(“a”)
1.4 synchronized 其他知识点
解释 : synchronized 提供了一种独占式的加锁方式 ,其添加和释放锁的方式由JVM实现
阻塞 : 当 synchronized 尝试获取锁的时候,获取不到锁,将会一直阻塞
谈谈 synchronized和ReenTrantLock 的区别
- 两者都是可重入锁
- synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
- ReenTrantLock 比 synchronized 增加了一些高级功能
- 等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件)
synchronized 与等待唤醒机制 (notify/notifyAll和wait)
等待唤醒机制需要处于synchronized代码块或者synchronized方法中 , 调用这几个方法前必须拿到当前对象的监视器monitor对象
synchronized 与 线程中断
线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用
1.5 多线程中的锁概念
1.5.1 锁按照等级分类
锁可以按照以下等级进行升级 : 偏向锁 -> 轻量级锁 -> 重量级锁 , 锁的升级是单向的
- 偏向锁
- 减少同一线程获取锁的代价 (在大多数情 况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得)
- 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程
- 偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁
- 轻量级锁
- 对绝大部分的锁,在整个同步周期内都不存在竞争
- 轻量级锁所适应的场景是线程交替执行同步块的场合
- 如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
- 自旋锁
- 假设等待后当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环
- 在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起
- 可以减少了线程上下文切换,但是增加了CPU消耗
- 重量级锁
1.5.2 锁的操作
锁清除 :
Java虚拟机在 JIT编译时 (可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间
Java 常见的锁
- synchronized 关键字 , 重量锁
- ReentrantLock 重入锁
- ReadWriteLock 读写锁
1.5.3 其他锁概念
内部锁 :
- synchronized : 锁对象的引用 , 锁保护的代码块
- 每个Java 对象都可以隐式地扮演一个用于同步的锁的角色 ,这些内置的锁被称为 内部锁 或 监视器锁 .
公平锁/非公平锁
- 公平锁是指多线程按照申请锁的顺序来获取锁,非公平锁指多个线程获取锁的顺序不是按照申请锁的顺序,有可能造成优先级反转或者饥饿现象,
- 非公平锁的优点在于吞吐量比公平锁大,ReentrantLock默认非公平锁,可通过构造函数选择公平锁,Synchronized是非公平锁。
可重入锁
可重入锁指在一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,ReentrantLock与Synchronized都是可重入的。
独享锁/共享锁
独享锁是指一个锁只能一个线程独有,共享锁指一个锁可被多个线程共享,对于ReadWriteLock,读锁是共享锁,写锁是独享所。
互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体实现。
乐观锁/悲观锁
- 乐观锁与悲观锁是看待同步的角度不同,乐观锁认为对于同一个数据的修改操作,是不会有竞争的,会尝试更新,如果失败,不断重试。
- 悲观锁与此相反,直接获取锁,之后再操作,最后释放锁。
分段锁
- 分段锁是一种设计思想,通过将一个整体分割成小块,在每个小块上加锁,提高并发。
1.6 锁的转换过程
1 | java复制代码对象头的变化可以看下图 , 说的很清楚了 @ https://www.cnblogs.com/jhxxb/p/10983788.html |
1.7 为什么锁会转换
1 | java复制代码// 这要从机制说起 , 每一种锁都有各自的特点 |
1.8 Synchoized 源码
1 | java复制代码 |
1.9 Synchoized 用法
1 | java复制代码public void operation(Integer check) { |
总结
逐步更新 , 逐步完善
更新日记
- V20210727 : 补充细节点 , 优化内容
- V20210813 : 补充深入细节 , Java 并发编程的艺术
- V20210814 : 补充汇编细节
参考文档
《深入浅出 Java 多线程》
《Java并发编程的艺术》
本文转载自: 掘金