• 无状态编程, lambda 表达式中传入的局部变量,为什么需要是不可变的(final)


    无状态编程

    说明

    1. @author JellyfishMIX - github / blog.jellyfishmix.com
    2. LICENSE GPL-2.0

    前言

    本文将会根据以下顺序进行叙述:

    • lambda 表达式中传入的局部变量,为什么需要是不可变的(final)?

    • 函数式编程提倡的无状态。

    • 无状态服务。

    lambda 表达式中传入的局部变量,为什么需要是不可变的(final)?

    场景演示

    image-20220722044229018

    public class Demo {
        public static void main(String[] args) {
            int i = 0;
            i = 2;
            Thread thread = new Thread(() -> {
                System.out.println(i);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里声明了一个变量 i,初值为 0,然后对 i 的值做了修改,这让 i 成为一个真正意义上的变量。于是 lambda 表达式在编译时无法通过,报错 “lambda 表达式中的变量 i 应该是常量,或效果上与常量相当的”。

    lambda 表达式中传入的局部变量,为什么需要是不可变的(final)?

    原因

    lambda 表达式多作为回调函数来使用,是延迟计算的。当回调函数真正被触发时,外部传入回调函数的局部变量可能已经被改变,这违背了使用者的预期。jdk 在编译环节就否定了外部传入 lambda 表达式一个变量,只能是常量(即 final 修饰),或效果上与常量相当的(声明赋初值后就没有被修改过的变量),在编译环节禁止了这种风险。

    解决方案

    image-20220722044704960

    public class Demo {
        public static void main(String[] args) {
            final int i = 0;
            Thread thread = new Thread(() -> {
                System.out.println(i);
            });
    
            int j = 1;
            Thread thread1 = new Thread(() -> {
                System.out.println(j);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    像这样,我们声明一个常量 i,和一个效果上与常量相当的(声明赋初值后就没有被修改过的变量)j。i 和 j 都满足 lambda 表达式对外部传入的局部变量的要求,编译可以通过。

    lambda 表达式语法糖,编译后的样子

    我们来看一下截图中的 lambda 表达式,编译后长什么样?

    # 这是编译用的命令
    javac Demo.java
    # 这是反编译用的命令
    javap -p Demo.class
    
    • 1
    • 2
    • 3
    • 4

    image-20220722050406986

    public class lambda.concurrent.map.Demo {
      public lambda.concurrent.map.Demo();
      public static void main(java.lang.String[]);
      private static void lambda$main$1(int);
      private static void lambda$main$0();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到,.java 文件的 thread1 中的 lambda 表达式,被编译成了当前类中一个叫 lambda$main$1(int); 的私有方法,原先 lambda 表达式中出现的外部局部变量,变成了此私有方法的入参。

    根据实验得出结论:

    1. lambda 表达式是一个语法糖,在编译后会生成一个当前类的私有方法。
    2. lambda 表达式内直接引用局部变量,本质是一种隐式传参,编译时会自动将引用的局部变量,放到根据 lambda 表达式生成的私有方法的参数列表中。

    javac 编译器中的"常量折叠"现象

    还有一点疑惑,.java 文件的 thread 中的 lambda 表达式,引用了一个常量。这个常量,为什么没体现在根据 lambda 表达式生成的私有方法 lambda$main$0(); 的参数列表中呢?

    我们来看一下 IDEA 为我们反编译出的更直观的 .class 文件:

    image-20220722052353604

    public class Demo {
        public Demo() {
        }
    
        public static void main(String[] args) {
            int i = false;
            new Thread(() -> {
            	// 这行注释是我自己加的。请注意,常量 0,编译后直接替换掉了原先的变量符号
                System.out.println(0);
            });
            int j = 1;
            new Thread(() -> {
                System.out.println(j);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们可以看到,之前的常量 0,编译后直接替换掉了原先的变量符号。这是 javac 编译器的一种叫 “常量折叠” 的现象,可以在编译时完成对常量的计算工作,使 JVM 在运行字节码时更快速。

    函数式编程提倡的无状态

    函数式编程提倡的无状态是什么?

    lambda 表达式是函数式编程的思想,而函数式编程是提倡无状态的,无状态是指什么呢?

    复述一段引用:夏梓耀 - 知乎

    一般所说的状态可视为 这样的三元组(引用,存储,值),reference 也可以叫 pointer,store 可看做是一个接受 reference 返回 value 的容器(具体实现可以是内存单元),value就是存储的值了;

    状态变化是指两方面变化了:1. reference 改变,2. reference 所指向的 value 改变。

    函数式编程提倡的无状态是指,“进去过的东西不因进去过而改变”,可以理解为不要在函数内修改由外部提供的变量的状态。

    无状态编程的优势?

    关于无状态编程的优势,可以看一下 stackOverflow 上的讨论: Advantages of stateless programming

    总结一下自己阅读讨论后的理解:

    1. 无状态编程在并发编程场景下优势明显,可变状态是多并发编程的天敌,会引发并发编程常见的竟态问题。如果默认情况下值是不可变的,开发者就无需担心某个线程会改变多个线程之间共享变量的值,因此不可变的值消除了竟态条件。由于没有竟态条件,所以不需要使用锁,因此不可变性也消除了与死锁相关的问题。
    2. 纯函数更容易测试和调试。

    比较简单的无状态实现方式,final 修饰变量,把变量标记为不可变。比如 String 类的设计,value 属性加了 fianl 修饰,这让 String 天然就是线程安全的。

    无状态服务

    1. 无状态就是指不能把例如用户登录信息这种数据,放在服务集群中的某台实例机器上,这样其他机器是感知不到这个数据的,只有这台机器自己能感知到。
    2. 用户的不同操作,都会给这个服务集群发请求,到底哪台机器处理这个用户的本次请求,是不确定的。
    3. 所以用户登录信息,必须得放在一个服务集群中任何一台机器都能感知到的地方,比如集中存储到 redis 中。
    4. 这样就把"用户登录信息"这个状态,从 单台机器 移动到了 集中式存储数据源,单台机器就是无状态的了。
    5. 一个服务集群中,每台机器运行的是同样的代码,此时称这个服务为无状态服务。
  • 相关阅读:
    上初三的小伙子做了个windows12网页版
    Apache Kylin的入门学习
    【kubernetes】Harbor部署及KubeSphere使用私有仓库Harbor
    大学四年如何有效的使用『牛客』平台
    AC自动机小结
    <Linux>基础IO_输出重定向&&缓冲区
    41. 【Android教程】Android 手势处理
    微信小程序三种授权登录以及授权登录流程讲解
    【Java第十八期】:#用Java模拟实现一个单向不带头不循环的链表
    线程创建、线程池创建相关理论知识整理
  • 原文地址:https://blog.csdn.net/weixin_43735348/article/details/127580605