最后
终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化
// java 代码
// instance 被 volatile关键字修饰
instance = new Singleton();
// 通过工具获取的JIT编译器生成汇编指令如下
0x01a3deld: movb $0…: lock add1 $x0,(%esp);
Lock 前缀的指令在多核处理器下会引发两件事情
-
将处理器缓存行的数据写回到系统内存
-
这个写回内存的操作会使在其他CPU缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存中的数据读到内存缓存(L1 L2或其他)后在进行操作,但操作完不知道何时写会内存。如果对申明了volatile的变量进行操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧值,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己的缓存是否过期了,当处理器发现自己缓存的数据对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器对这个数据进行修改时,就会重新从内存中读取数据到缓存中。
=====================================================================================
在多线程并发编程中synchronized一直是元老角色,很多人称呼它为重量级。但是随着Java SE 1.6对synchronized进行了各种优化之后,有些情况synchronized它不在那么重了。接下来阐述的知识点是关于偏向锁、轻量级锁,以及锁的存储结构和升级过程。
synchronized在Java中三种表现形式
-
对于普通同步方法,锁的是当前实例对象
-
对于静态同步方法,锁的是类的class对象
-
对于同步方法块,锁的是synchronized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时,必须释放锁。name锁到底存在哪里呢?锁里面存的又是什么信息呢?
在JVM规范中可以看到synchronized在JVM中的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,两者实现的细节不一样。代码块使用monitorenter和monitorexit指令实现,而方法同步使用的是另外一种情况,这个在JVM规范中并没有讲解。但是,方法的同步也可使用这两个指令来实现。
-
monitorenter指令是在编译后插入到同步代码块的开始位置
-
monitorexit指令插入到方法结束的位置和异常处
-
JVM要保证每个monitorenter必须有与之对应的monitorexit配对
-
任何对象都有一个monitor与之关联
-
当一个monitor被持有后,它将处于锁定状态
-
线程执行monitorenter指令时,将会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁
synchronize用的锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数据类型,则虚拟机用2个字宽存储对象头 。在32位虚拟机中1字宽等于4字节,即32bit。
Java对象头的长度
| 长度 | 内容 | 说明 |
| — | — | — |
| 32/64bit | Mark Word | 存储对象的hashCod或锁信息 |
| 32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
| 32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Java对象头的Mark Word默认存储的是对象的hashCode、分代年龄和锁标志位。32位JVM的Mark Word的默认存储结构如表
Java对象头存储结构
| 锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
| — | — | — | — | — |
| 无锁状态 | 对象的hashCode | 对象的分代年龄 | 0 | 01 |
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,内容较为复杂,图先不讲了,有兴趣的可以查看相关书籍信息
Java SE 1.6 为了减少获得锁和释放锁带来性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。注意:锁的升级是不可逆的,意味着偏向锁升级为轻量级锁之后是不能降级为偏向锁的。
2.1.1 偏向锁
HotSpot的作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取的,为了让线程获得锁的代价更低,引入了偏向锁。 注意:这个是设计偏向锁的原因和解决思路
2.1.1.1 偏向锁的获取
当一个线程访问同步代码块并获取锁时,会在对线头和栈帧中的锁记录中存储锁偏向的线程ID,以后该线程在进入和退出同步代码块的时候,不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对线头Mark Word里是否存储了当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁标识是否被设置成立1(表示当前是偏向锁):如果没有设置则使用CAS竞争锁;如果这事了,则尝试使用CAS将当前对象头的偏向锁指向当前线程。
2.1.1.2 偏向锁的撤销(非常妙这里)
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁是,持有偏向锁的线程才会释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁标记活着标记该对象不适合作为偏向锁,最后唤醒暂停的线程
2.1.1.3 关闭偏向锁
Java 1.6 1.7偏向锁是默认开启的,但是它在应用程序启动几秒钟后才会激活,我们可以修改JVM参数来关闭延迟,或者确定应用程序里所有的锁通常情况下都是出于竞争状态,可以直接关闭偏向锁
关闭偏向锁延迟
-XX:BiasedLockingStartupDelay=0
关闭偏向锁 程序默认进入轻量级锁状态
-XX:-UseBiasedLocking=false
2.1.2 轻量级锁
2.1.2.1 轻量级锁加锁
线程在执行同步代码块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试自旋获得锁。
2.1.2.2 轻量级锁解锁
轻量级锁解锁时,使用的是CAS操作将Displaced Mark Word替换回对象头,如果成功则表示没有竞争;如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。 注意:由于自旋过程消耗CPU,为了避免无用的自旋,一当升级为重量级锁,那么就不会再恢复到轻量级锁状态。当前锁出于重量级锁状态时,其他线程尝试获取锁,都会被阻塞,当持有锁的线程释放锁后会唤醒这些线程,重新进行锁的争夺。
2.1.3 锁的优缺点对比
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 偏向锁 | 加锁和解锁过程不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差异 | 如果线程存在锁竞争,会带来额外的锁撤销开销 | 适用于只有一个线程访问同步代码块 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步代码块执行速度非常快 |
| 重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较长 |
在Java中可以通过锁和循环CAS的方式实现原子操作
3.1 使用循环CAS实现原子操作
JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现。自旋CAS实现的基本思路就是循环进行CAS操作知道成功为止,示例代码实现一个安全的计数器和非安全的计数器。
package com.liziba;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
-
@auther LiZiBa
-
@date 2021/2/28 17:39
-
@description: 计数器实现
**/
public class Counter {
// 安全计数器统计数
private AtomicInteger atomicInteger = new AtomicInteger(0);
// 非安全计数器统计数
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List threads = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 非安全计数器
cas.count();
// 安全计数器
cas.safeCount();
}
});
threads.add(t);
}
// 启动线程
threads.forEach(t -> t.start());
// join等待所有线程执行完毕
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Java面试核心知识点笔记
其中囊括了JVM、锁、并发、Java反射、Spring原理、微服务、Zookeeper、数据库、数据结构等大量知识点。
Java中高级面试高频考点整理
最后分享Java进阶学习及面试必备的视频教学
08)]
Java中高级面试高频考点整理
[外链图片转存中…(img-zoJHN60Y-1714845537408)]
[外链图片转存中…(img-D9k4gOty-1714845537408)]
最后分享Java进阶学习及面试必备的视频教学
[外链图片转存中…(img-tMqrdk6f-1714845537408)]