无状态编程, 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);
        });
    }
}

这里声明了一个变量 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);
        });
    }
}

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

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

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

# 这是编译用的命令
javac Demo.java
# 这是反编译用的命令
javap -p Demo.class

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();

可以看到,.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);
        });
    }
}

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

函数式编程提倡的无状态

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

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

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

一般所说的状态可视为 <reference, store, value> 这样的三元组(引用,存储,值),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. 一个服务集群中,每台机器运行的是同样的代码,此时称这个服务为无状态服务。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JellyfishMIX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值