你真的懂匿名类、lambda、方法引用?先过了这道题再说!!!

3 篇文章 0 订阅
2 篇文章 0 订阅

先给出一道很简洁的小段 Java 程序,你看一下是否能答出正确结果。

在类中有一个静态变量;
静态方法块中,抛出子线程修改变量的值,然后等待子线程执行结束;
main 方法查看变量的值。

public class LambdaTest {
    // 静态变量初始为false
    static boolean b = false;
    static {
        // 抛出子线程将变量改为true
        Thread t = new Thread(() -> b = true);
        t.start();
        // 等待子线程执行完成
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 输出变量是否被修改
        System.out.println(b);
    }
}

最好不要先偷窥答案。
我们先想一想可能结果是什么:
比如

  1. 修改成功,打印 true
  2. 修改不成功,打印 false
  3. 编译错误(猜的)
  4. 程序卡死(我也不知道,你自己想)
  5. 程序抛出异常。。

也许还有其他答案吧,需要你们自己分析判断。

接下来,就是见证奇迹的时刻:
Lambda
好吧,程序卡死了。

不知道你猜对了没有。
假设你碰巧猜对了,
那我再对代码稍作修改,你看一下是否还能继续猜对

尝试将 lambda 表达式改为空

Thread t = new Thread(() -> {});

总代码

public class LambdaTest {
    // 静态变量初始为false
    static boolean b = false;
    static {
        // 这次我什么都不干总可以了吧
        Thread t = new Thread(() -> {});
        t.start();
        // 等待子线程执行完成
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 输出变量
        System.out.println(b);
    }
}

lambda
不要懵逼,我再改一改

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("你干嘛呢");
    }
});

总代码

public class AnonymousInnerClass {
    static boolean b = false;
    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("你干嘛呢");
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

匿名内部类
不错,执行完了
再来:

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("子线程运行");
        b = true;
    }
});

完整代码

public class AnonymousInnerClass {
    static boolean b = false;
    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程运行");
                b = true;
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

匿名内部类
诶?
子线程运行了?
可是怎么还是卡死了。。。。

不急,继续
改成方法引用

// 改成方法引用
Thread t = new Thread(System.out::println);

完整代码

public class MethodQuote {
    static boolean b = false;
    static {
        Thread t = new Thread(System.out::println);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

方法引用
它又奇怪的执行结束了。。。
(当然全答出来的大佬就不要来吐槽我了)

解析

经过百般波折,你们可能已经蒙了。
这其中主要是涉及到

  • 匿名内部类
  • lambda 表达式
  • 方法引用

我们发现,这三种方式,抛出的线程,执行的结果状态都不一样。

可能很多人想当然的认为,lambda 表达式,和匿名内部类是同一种实现,只是写法不同。
然而实际上不是的(从这里面也可以体现出)。

下面我们一步步来分析。

为了方便定位,我把 new 出来的子线程命了一个名字:叫子线程
定位
然后我们打印出线程信息,发现,子线程还是 RUNNABLE 的状态。
但是,我们仔细一看:
cpu=0.00ms
说明它根本就还没有执行 !!!
在这里插入图片描述
然后我们再转头一看主线程,发现它已经陷入了 WAITING 中,
也就是说,我们的程序已经被卡死了。
在这里插入图片描述
为了理清到底是哪些地方出了问题,我们在程序的各个点打印输出,来进行错误排查

public class LambdaTest {
    static {
        System.out.println(Thread.currentThread().getName() + ":static方法开始");
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":子线程运行");
        }, "子线程");
        t.start();
        try {
            System.out.println(Thread.currentThread().getName() + ":子线程join");
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":static方法结束");
    }
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ":main方法运行");
    }
}

print排查

  • 我们可以发现,类的初始化是由 main 线程执行的。
  • 然后,程序运行到 join 之后,整个程序卡死。
  • 我们之前也看过,main 线程是处于 WAITING 状态的

所以肯定是运行的子线程哪里有问题。

我们点开 join 方法,也可以发现,join 的本质也就是 waitjoin
我们 debug 一下,会发现线程在执行结束后,最终会退出,唤醒那些 join 了自己的线程
在这里插入图片描述
在这里插入图片描述
所以我们就要去弄明白,为什么子线程没有成功执行。
这就涉及到 lambda 表达式 和 匿名内部类 之间的区别了。

我们先看匿名内部类:
我们打开编译过后的 class 所在的文件夹,打开过后,发现有两个 class 文件,
一个是我们之前写的测试类;
还有一个就是我们的匿名内部类,它的名字是在所在的类的名字后面加 $1
如果还有其它匿名内部类的话,就依次是 $1、$2、$3、$4……
匿名内部类
也就是说,我们的匿名内部类,在编译的时候,就已经存在了。
但是我们的 lambda表达式、方法引用,在编译的时候不会产生类,而是运行时动态产生的。
(因为我们编译过后没有产生其它 class)
在这里插入图片描述
我们目前已经开始发现一点区别了。
一个是静态生成类,编译时已经存在;
一个是动态生成类,只有在运行时才会生成类。

我们继续探究,
点开编译后 class 的指令,我们可以发现:
在匿名内部类中,有 invokespecial 字节码指令。
匿名内部类
我们翻开官方文档:
上面说明了这是一个初始化的指令。
文档
其实我们在看到 class 字节码指令的时候,后面写了 init,我们也就能想出来了。
官方说明是为了确认。
init
但是到了我们的 lambda 表达式的时候,我们可以看到:
在 class 字节码中表示的是 invokedynamic 动态指令
lambda表达式
区别显而易见,匿名内部类由于编译时已经产生,所以完全可以直接初始化,从而创建的线程可以正确执行匿名内部类的方法。
匿名内部类
不过我们之前的题目中有一个示例,匿名内部类也无法成功执行,我们来看一下这又是为什么:
匿名内部类
刚才我们探讨过了,采用匿名内部类是可以成功执行的,因为在编译时,类文件已经产生。

但是这里,我们发现了一个点,该匿名内部类引用了我们外部类的静态变量。

那我现在我们来分析程序执行流程。

  1. 首先,程序创建了匿名内部类的对象,然后创建子线程去跑这个 Runnable 对象。
  2. 线程启动后,开始执行,主线程调用 join 等待子线程结束
  3. 子线程成功打印了 “子线程运行” 这句话(我们之前的执行结果便是如此)
  4. 但是子线程访问到 外部类的变量

这就是我们要注意的点了,我们要去访问一个类的时候,这个类必须是初始化完成的 !!!
(按照道理我们不可以去访问才初始化一半的对象,这时候 static 方法都没执行完)

所以,子线程此刻便在等待,主线程完成外部类的初始化;
但是,这时候主线程还在等子线程执行完。。。。

所以,就产生了死锁。

所以,不管是对于匿名内部类,还是其他任意的一个类,只要去访问其他类,就必须等到那个类完成初始化。
这也就是我们在类中访问了外部类变量而导致死锁的原因。

我们继续回到 lambda 表达式,我们之前已经发现,它在编译的时候,根本没有创建出类。
也就是类是运行时生成。

这时候,我们不管有没有在类的方法中引用外部类的变量,都没有什么实质性的作用。
因为这时候要动态生成内部类,就必须先初始化外部类。
在这里插入图片描述
这时候,我们已经区分好了匿名内部类和 lambda 表达式的区别,虽然都是生成类,但是:

  • 匿名内部类是编译时静态生成
  • lambda 表达式是运行时动态生成类

但是,我们可别忘了,我们还有一个方法引用没有讲
我们记得当时的代码示例,在方法引用的时候,程序是可以成功执行结束的。
但是我们点开一看,发现却是 invokedynamic
在这里插入图片描述
怎么感觉我在啪啪打脸??
不怕,我们继续点进去看一下。。
我们可以发现,由于 Runnable 引用的就是一个 pringln 方法,
而 System.out 对象,早就以及初始化完成了,所以根本不用去担心初始化内部类而要先初始化外部类的问题。
在这里插入图片描述
因而,采取方法引用的方式,也可以执行成功。

总结

现在,想必你对匿名内部类、lambda 表达式、方法引用,都有一定理解了。

其实,对于这些 Java 的知识,不仅仅只是简单使用即可。
更多的时候,要去弄明白其中的原由,仍需要对一些源码的掌握,以及对 Java 虚拟机的理解,操作系统底层的一些了解方可。

因为很多时候,代码的逻辑是确实不存在问题的,但是,对于一些底层的原理,会或多或少的影响到我们程序的执行。
因此,为了超越一个 CRUD 程序员,我们必须去对知识的横向、纵向扩展。

这样,才能掌握彻底,理解深刻,方能运用自如。

  • 17
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值