大家好,我是此林。

往期回顾:

Java 字节码进阶1:深入解析 class 文件结构-CSDN博客

Java 字节码进阶2:为什么 switch 比 if-else 高效(阿里代码规约)?-CSDN博客

Java 字节码进阶3:面向对象多态在字节码层面的原理?-CSDN博客

前三章,我们深入分析了 class 字节码文件的十大组成、介绍了阿里代码规约中 switch-case 比 if-else 高效的字节码原理、以及多态在字节码层面的原理。

今天,我们来深入分析 泛型擦除、synchronized 的字节码原理。

1. 泛型的优点

Java 泛型是 JDK5 引入的一个新特性。

Java 泛型是 Java 提供的一种 类型参数化机制,可以让类、接口或方法在定义时不指定具体类型,而在使用时再指定具体类型。它的核心目的就是 提高代码的安全性、复用性

1.1. 安全性

泛型可以在编译阶段就检查类型,而不是等到运行时抛出 ClassCastException

例子:

// 没有泛型
List list = new ArrayList();
list.add("Hello");
Integer number = (Integer) list.get(0); // 编译通过,但运行时出错

// 使用泛型
List<String> list2 = new ArrayList<>();
list2.add("Hello");
String str = list2.get(0); // 编译时就能检查类型,不会出错

泛型使得在编译期就能发现类型转换错误,提高安全性。

1.2. 复用性

泛型可以写一个类或方法,支持多种类型,而不用为每种类型重复写一份代码。

例子:

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

// 使用
Box<Integer> intBox = new Box<>();
intBox.set(123);

Box<String> strBox = new Box<>();
strBox.set("Hello");

可见,泛型使得一个类就可以处理多种类型的数据。

2. 泛型擦除

我们从字节码的角度分析泛型擦除,下面以泛型类 Pair 为例。

public class Pair<T> {
    
    public T first;
    public T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }
}



public void foo(Pair<String> pair) {
    String tmp = pair.first;
}

其中,foo 方法的字节码对应如下:

0: aload_1
1: getfield      #2    // Field left:Ljava/lang/Object
4: checkcast     #4    // class java/lang/String
7: astore_2
8: return

aload_1 表示把方法参数 Pair<String> pair 压入操作数栈。

getfield 表示获取 pair 的字段属性 first,可以看到 first 的类型并不是 String,而是 Object。

checkcast 表示检查 pair.first 的类型是否能强转映射到 String。若类型不匹配,抛出 ClassCastException。

astore_2  表示把 pair.first 的值存储到局部变量 tmp 里。

return 方法结束。

所以实际上 javac 编译后成字节码之后,泛型是会被擦除的,泛型都会被替换为 Object 类型。编译器会在需要的地方自动插入 强制类型转换 (checkcast),以保证类型安全。

也就是上面这段代码等同于下面的代码:

public class Pair {
    
    public Object first;
    public Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
}



public void foo(Pair pair) {
    String tmp = (String) pair.first;
}

不过需要注意的是,说 “ 泛型在编译之后会被擦除成 Object ” 并不是很严谨,

严格来说应该是: 类型参数会被 擦除(Type Erasure),即替换为它的上界(如果没有显式指定上界,则是 Object 

例子:如果 Pair<T> 声明为 class Pair<T extends Number>,那么泛型会被擦除为 Number,而不是 Object。

3. 泛型的缺点

1. 泛型类并没有自己独有的 class 对象,比如并不存在 List<Integer>.class 和 List<String>.class,只有 List.class,所以下面代码两个方法的方法签名在编译后是相同的,无法同时存在会报错。

public void print(List<String> list)

public void print(List<Integer> list)

2. 泛型不用用 Java 的基础数据类型,比如不能有 List<int>,只能有 List<Integer>。这是因为泛型会擦除为 Object,原始的 int 不能存储到对象类型 Object 中。

3. Java 不能捕获泛型异常,以下面的代码为例。

public <T extends Throwable> void foo() {
    try {
    } catch (T e) {
    }
}

因为 JVM 的异常处理是通过 异常表 来实现的,如果要捕获的异常在编译器无法确定,就无法生成对应的异常表。

泛型的内容到此就告一段落了,接下来我们看 synchronized 字节码原理。

4. synchronized 字节码原理

synchronized 是 Java 中的关键字,用于定义一个 临界区

临界区 是指一次只能被一个线程执行的代码片段。

synchronized 保证方法和代码块在同一时刻只有一个线程可以进入临界区。

synchronized 关键字可以修饰在三个地方:代码块、实例方法、静态方法。

但 synchronized 本质上是作用在对象上。

修饰在代码块:作用于括号里的对象

修饰在实例方法:作用于当前 this 实例对象

修饰在静态方法:作用于当前 Class 对象

4.1. 修饰在代码块


public class A {
    public static void main(String[] args) {
 
    }
 
    public void test() {
        synchronized (this) {
            System.out.println("test");
        }
    }
}

上面这段代码用 jclasslib 插件反编译看下字节码。

执行 monitorenter 代表去抢占 monitor 对象,抢到了 monitor 对象就代表持有了锁。

monitorexit 也就很好理解了,是释放锁的意思。

为什么 monitorexit 要执行两次呢?

因为代码如果出现异常了,也需要解锁,否则就死锁了。

从字节码的角度,我们也就可以知道为什么 synchronized 不需要手动解锁了。

因为编译器生成的字节码里已经给我们考虑好了,异常情况也考虑到了。

4.2. 修饰在方法上

public class A {
    public static void main(String[] args) {
 
    }
 
    public synchronized void test() {
        System.out.println("test");
    }
}

同样的,这段代码我们再反编译一下。 

不过,这一次好像没有自动加 monitorenter 和 monitorexit 指令啊。

别急,你看看当前方法的访问标志。这里是 public synchronized 。

这样 JVM 就知道这个方法是被 synchronized 标记的,在进入方法前后会进行加锁解锁操作。

对比一下之前修饰代码块的访问标志。

所以 synchronized 修饰代码块和修饰方法在字节码层面是不一样的,修饰代码块会自动加上 monitorenter 和 monitorexit 指令,修饰方法时会在方法的访问标志上做标记。 

关于访问标记,在这篇文章中已经叙述过了,这里不多赘述:

Java 字节码进阶1:深入解析 class 文件结构-CSDN博客

synchronized 在字节码层面的原理就讲到这里了,如果还想更加深入 synchronized 的底层原理,可以看看这篇文章:

从 JVM 源码(HotSpot)看 synchronized 原理_synchronized源码-CSDN博客

这里详细讲述了 Java 对象结构、synchronized 的锁升级流程、深入 Hotspot JVM 源码。

今天的分享就到这里了,相信你一定有所收获!

我是此林,关注我吧,带你看不一样的世界!

更多推荐