类加载与字节码技术
类加载阶段
1. 加载
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
-
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class ,作用是把 klass 暴露给 java 使用
-
_super 即父类
-
_fields 即成员变量
-
_methods 即方法
-
_constants 即常量池
-
_class_loader 即类加载器
-
_vtable 虚方法表
-
_itable 接口方法表
-
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
instanceKlass 这样的【元数据】是存储在方法区( 1.8 后的元空间内),但 _java_mirror是存储在堆中
2. 链接
验证
在这个阶段会验证类是否符合JVM规范,并进行安全性检查。
如果对class文件中的魔数进行了修改,那么会出现如下报错:
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
...
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
准备
为static变量分配空间,并设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
例如
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
}
在编译时,如果我们在另一个类中使用了 getName 方法,那么编译器会生成一个符号引用,表示对 Person.getName() 方法的引用。这个引用是抽象的,不包含具体的内存地址。
在解析阶段,Java虚拟机会将这个符号引用翻译成运行时可用的引用。具体步骤包括:
-
定位
Person
类: Java虚拟机会查找并定位到Person
类的字节码文件,以便在内存中加载它。 -
找到
getName
方法: Java虚拟机会在Person
类中找到getName
方法,并确定它的内存地址或偏移量。 -
将符号引用转化为直接引用: 解析阶段将符号引用
Person.getName()
转化为一个直接引用,该引用指向getName
方法的内存地址或偏移量,以便在运行时可以直接调用该方法。
这个过程确保了在运行时,Java虚拟机可以正确地找到并执行 Person.getName()
方法,而不会出现引用错误。解析阶段的主要目的是确保符号引用能够顺利转化为直接引用,从而保证程序的正确性和可执行性。
3. 初始化
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
以下情况不会导致初始化
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 访问 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
public final class Singleton {
private Singleton() { }
// 内部类中保存单例
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
-
Singleton
类被声明为final
,意味着它不能被继承。 -
private Singleton() { }
:私有的构造函数确保该类无法被外部直接实例化。 -
private static class LazyHolder { ... }
:这是一个静态内部类。静态内部类不会在外部类加载时被初始化,只有当它被实际调用时才会被加载和初始化。这种延迟加载的特性使得它非常适合单例模式的实现。 -
static final Singleton INSTANCE = new Singleton();
:在LazyHolder
静态内部类中创建了一个静态、final的Singleton
实例。这个实例在类加载和初始化时被创建,因此是线程安全的。 -
public static Singleton getInstance() { ... }
:这是获取单例实例的静态方法。当该方法被调用时,它会返回LazyHolder.INSTANCE
,触发LazyHolder
类的加载和初始化,从而创建单例实例。由于类加载是线程安全的,所以该模式保证了在多线程环境下也能正确地返回单例实例。
类加载器
以JDK8 为例:
双亲委派机制:从下往上询问,从上往下加载
- 当一个类加载器试图加载一个类时,首先会检查自己有没有加载这个类,如果有,则直接返回已加载的类。
- 如果没有,则向上委托给父类加载器。父类加载器也会按照同样的方式检查是否已加载,如果没有,则再将请求委托给自己的父类加载器,一直向上委派,直到达到最顶层的Bootstrap ClassLoader
- 如果所有的类加载器都没有加载过这个类,那么Bootstrap ClassLoader会尝试在自己的类加载路径中查找这个类并加载,如果没有找到便向下委派给Extension ClassLoader,一直到自定义类加载器
1. 启动类加载器
用 Bootstrap 类加载器加载类:
package cn.itcast.jvm.t3.load;
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行:
package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}
输出:
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null
- Bootstrap ClassLoader是cpp编写,Java无法直接访问,因此返回为null
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath:<new bootclasspath>
- java -Xbootclasspath/a:<追加路径>(后追加
- java -Xbootclasspath/p:<追加路径>(前追加
2. 扩展类加载器
扩展类加载器主要用于加载位于 $JAVA_HOME/lib/ext
目录下的 JAR 文件和类。这个目录是用于存放Java标准库之外的扩展库的地方。
如果有两个同名的类,一个位于 Class Path下,另一个位于$JAVA_HOME/lib/ext下,那么加载结果会是什么?
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("classpath G init");
}
}
执行
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出:
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
将这个类稍做修改并打好jar包拷贝至JAVA_HOME/lib/ext
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
重新执行
输出:
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
因为双亲委派机制的缘故,Application ClassLoader类加载器首先会委派给自己的父类加载器Extension ClassLoader,然后ext检查自己是否加载了这个类。ext会在自己的类加载路径下找到这个类,于是直接返回。因此App类加载器也就没用上,返回的是ext类路径下的类。
3.双亲委派模式
当试图加载一个类时,会执行以下代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
工作流程:
- Application ClassLoader会调用 findLoadClass() 方法,检查该类是否已加载,如果返回值为null,则进入下一步
- 当返回值为 null 时,判断是否存在父类,如果有,则委派给父类加载器(这里是Extension ClassLoader)
- Extension ClassLoader 调用 findLoadClass() 方法,检查该类是否已加载,如果返回值为null,则进入下一步
- 当返回值为 null 时,判断是否存在父类。因为 Extension ClassLoader 的父类加载器无法直接被Java编译器访问,因此返回值为null,进入 else 分支,委派 Bootstrap Class Loader,调用 findBootstrapClassOrNull() 方法
- findBootstrapClassOrNull() 方法是一个本地方法,它会在JAVA_HOME/jre/lib 目录下查找这个类,如果没有则返回null
- 继续往下执行,Extension ClassLoader 发现仍然没有找到要加载的类(c 仍为 null),则会调用 findClass() 方法,试图在自己的类加载路径JAVA_HOME/jre/lib/ext 下查找这个类
- 如果仍没有找到,则抛出 ClassNotFoundException
- ClassNotFoundException 被 Application ClassLoader 捕捉到,因为 catch 块中内容为空,因此继续往下执行。c 仍未 null ,Application ClassLoader 调用自己的 findClass() 方法,在 ClassPath 类路径下查找这个类。
- 如果找到便返回这个类,如果仍没有找到,继续抛出 ClassNotFoundException
4. 线程上下文类加载器
- 线程上下文类加载器是从JDK1.2开始引用的,getContextClassLoader()与setContextClassLoader(ClassLoader cl),用来分别获取和设置上下文类加载器
- 如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器.
- java应用运行时的初始线程的上下文类加载器是应用程序类加载器.在线程中运行的代码可以通过该类加载器来加载类与资源
Class.forName("com.mysql.jdbc.Driver")
也能让 com.mysql.jdbc.Driver 正确加载,这是为什么?
首先,Java定义的JDBC接口中提供了DriverManager类,它的类加载器是Bootstrap ClassLoader,它会自动查找合适的驱动程序。Java定义的JDBC接口位于JDK的rt.jar
中(java.sql
包),因此这些接口会由Bootstrap ClassLoader进行加载。
而这个接口的实现,即数据库厂商提供的Driver驱动通常情况下位于 ClassPath 下,这超出了Bootstrap ClassLoader的加载范围,无法让它来加载。想要加载Driver驱动的话只能用 AppClassLoader 或者自定义类加载器。
但是这就违反了双亲委派机制,因为父类加载器无法委派给子类加载器。为此,Java提供了一个 ThreadContextClassLoader,即线程上下文类加载器。
在DriverManager.getConnection(“jdbc:mysql://localhost:3306/mytestdb”,“username”,“password”);方法中,调用 Thread.currentThread().getContextClassLoader(); 使用了当前线程使用的类加载器(默认是AppClassLoader)去加载jar包中的Driver驱动。
这样做的好处就是契合了 SPI(Service Provider Interface) 的思想:开发者提供接口或抽象类,不同的实现提供者为这个接口提供具体的实现。
SPI工作原理
定义接口或抽象类:首先,开发者定义一个接口或抽象类,用于描述一组规范或服务。
提供实现:接着,不同的实现提供者编写符合接口或抽象类规范的具体实现。这些实现通常以独立的JAR文件或模块的形式存在。
配置文件:SPI机制通常需要一个配置文件,例如
META-INF/services/
目录下的配置文件,用于指定实现提供者的类名。这个文件的内容是一个实现类的全限定名列表。
运行时加载:应用程序在运行时可以使用SPI机制来加载指定的实现类,这些实现类会自动被实例化并注入到应用程序中。
在JDBC这个案例中,也就是Bootstrap ClassLoader 加载了Java提供的JDBC接口,然后调用线程上下文类加载器,返回底层去加载具体的数据库驱动实现。
5. 自定义类加载器
- 想加载非 classpath 随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法
- 注意不是重写 loadClass 方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
运行期优化
1. 即时编译
分层编译
public class JIT1 {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
它的部分运行时间如下
可以看到,有明显的两次速度提升,从50000提升到20000,以及再次提升到800
之所以会这样,是因为
- 0 层,解释执行(Interpreter)
- 1 层,使用 C1 即时编译器编译执行(不带 profiling)
- 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
- 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
- 4 层,使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT 会根据平台类型,生成平台特定的机器码
方法内联
private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
System.out.println(9 * 9);
System.out.println(81);
字段优化
-
字段排列和对齐: 编译器可以对类的字段进行排列和对齐,以减少内存浪费和提高访问速度。例如,编译器可以将相同数据类型的字段放在一起,以减少填充字节。此外,对齐字段可以使其更容易地加载到寄存器中,从而提高访问速度。
-
字段压缩: 对于某些数据类型,编译器可以使用更紧凑的表示方式,以减少内存使用。例如,可以将布尔值压缩为1位而不是1字节。
-
字段内联: 编译器可以将字段内联到使用它们的方法中,从而减少字段访问的开销。这在某些情况下可以减少对对象引用的依赖,提高访问速度。
-
去除不必要的字段: 编译器可以检测和删除不再使用的字段,以减少对象的内存占用。
-
字段访问优化: 运行时系统可以通过使用寄存器等优化技术来加速字段的访问。这包括将字段加载到高速缓存中以提高访问速度。
2. 反射优化
-
缓存反射对象: 将反射操作的结果缓存起来,以避免重复的反射查找和调用。
-
使用反射池: 可以使用反射池(Reflection Pool)来重用反射对象,从而减少对象的创建和销毁开销。
-
直接访问字段和方法: 如果性能要求高,可以使用直接字段和方法访问,而不是反射调用。
-
避免不必要的反射: 尽量避免不必要的反射操作,可以通过设计良好的类和接口来减少反射的需求。
-
使用替代方案: 在某些情况下,可以考虑使用其他技术或框架来替代反射,例如使用代码生成或代理模式。
内存模型
1. Java内存模型
Java内存模型(Java Memory Model,JMM)是一种规范,用于定义多线程并发访问共享内存时的行为和规则。JMM规定了在多线程环境下,如何保证线程之间的可见性、原子性和有序性,以避免出现数据竞争和不确定性行为。
1.1 原子性
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i
Java的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:
在单线程下,以上8条指令是顺序执行的,所以不会出现问题。
但是在多线程下,这8条指令有可能交错运行:
出现负数的情况:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
出现正数的情况:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
而解决办法便是使用 synchronized 关键字进行上锁:
synchronized( 对象 ) {
//要作为原子操作代码
}
使用 synchronized 关键字可以解决并发问题
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
注意:上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。
2 可见性
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
这个程序很有可能无法正常结束,因为main 线程对 run 变量的修改对于 t 线程不可见,从而导致了 t 线程无法停止。
对此程序进行分析:
![](https://i-blog.csdnimg.cn/blog_migrate/1436990634a73e53623c86bdc43f0ed4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/640581485b3d5ed96c02a0a094ae6552.png)
![](https://i-blog.csdnimg.cn/blog_migrate/eed1013ca0592abdf6c6fe400cf64fca.png)
注意synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?这是因为在System.out.println()中也使用了 synchronized 关键字
3 有序性
3.1 问题
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
对于以上一段代码,可能的结果有几个?
![](https://i-blog.csdnimg.cn/blog_migrate/40180090b4ffc284c57fe25212e48523.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6d736d7c2c75b6610bd5d2b2a01fa7a2.png)
3.2 解决办法
使用volatile 修饰的变量,可以禁用指令重排
对原代码中的 ready 变量使用 volatile 进行修饰后重新测试:
可以看到 0 的结果不再出现了。
3.3 有序性理解
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;
i = ...; // 较为耗时的操作
j = ...;
也可以是
j = ...;
i = ...; // 较为耗时的操作
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton;
时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)
4 CAS与原子类
4.1 CAS
// 需要不断尝试
while(true) {
int 旧值 = 共享变量 ; // 比如拿到了当前值 0
int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
/*
这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
compareAndSwap 返回 false,重新尝试,直到:
compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
*/
if( compareAndSwap ( 旧值, 结果 )) {
// 成功,退出循环
}
}
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
4.2 乐观锁与悲观锁
-
悲观锁(Pessimistic Locking): 悲观锁是一种悲观的假设,它认为在多线程环境下,资源会发生冲突,因此在访问资源之前会先对资源进行加锁,以防止其他线程同时访问。在悲观锁的机制下,当一个线程获得了锁后,其他线程必须等待锁被释放才能访问资源。典型的悲观锁实现包括数据库的行级锁和表级锁,以及Java中的
synchronized
关键字。 -
乐观锁(Optimistic Locking): 乐观锁是一种乐观的假设,它认为资源在多线程环境下不会经常发生冲突。因此,在访问资源时不会加锁,而是先尝试进行操作,然后在更新资源时检查是否有其他线程已经修改了资源。如果没有冲突,操作继续执行;如果发现冲突,就需要回滚操作或者重试。典型的乐观锁实现包括版本号控制和CAS(Compare and Swap)操作。
关键区别:
- 悲观锁认为冲突是常态,所以在访问资源前先加锁,其他线程必须等待。
- 乐观锁认为冲突是少数情况,所以不加锁,而是在更新时检查冲突。
4.3 原子操作类
// 创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); // 获取并且自增 i++
// i.incrementAndGet(); // 自增并且获取 ++i
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndDecrement(); // 获取并且自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
5. synchronized 优化
5.1 轻量级锁
5.2 锁膨胀
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
流程:
- 线程1访问同步块,把 obj 对象头中的Mark复制到线程1的锁记录中
- CAS修改 Mark 为线程1的锁记录地址
- 成功加锁(轻量级锁)
- 线程1执行同步块
- 线程1执行同步块
- 此时,线程2访问同步块,并把 Mark 复制到线程2的锁记录中
- 线程1执行同步块
- CAS修改 Mark 为线程2的锁记录地址
- 线程1执行同步块
- 加锁失败
- 线程1执行同步块
- CAS修改 Mark 为重量级锁(由 00 变为 10 )
- 线程1执行同步块
- 线程2阻塞等待
- 线程1执行完毕
- 线程2阻塞等待
- 线程1解锁失败
- 线程2阻塞等待
- 线程1释放重量级锁,唤起阻塞线程竞争
- 线程2阻塞等待
- 线程2竞争重量级锁
- 线程2竞争成功(加锁)
5.3 重量锁
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)
- Java 7 之后不能控制是否开启自旋功能
自旋重试成功
自旋重试失败
5.4 偏向锁
偏向锁(Biased Locking)是Java中锁优化的一种机制,旨在减少低竞争情况下的锁操作的性能开销。它的核心思想是:在没有竞争的情况下,将锁偏向于第一个获取锁的线程,使得该线程后续再次进入同步块时不需要重新竞争锁,而是直接获得锁,从而提高了性能。
- 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
- 访问对象的 hashCode 也会撤销偏向锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,
- 重偏向会重置对象的 Thread ID
- 撤销偏向和重偏向都是批量进行的,以类为单位
- 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
- 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
5.5 其他优化
1. 减少上锁时间
同步代码块中尽量减少无关代码,只保留关键代码,以减少上锁时间
2. 减少锁的粒度
- ConcurrentHashMap
- LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时 候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
- LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
3. 锁粗化
new StringBuffer().append("a").append("b").append("c");