无状态编程
说明
- @author JellyfishMIX - github / blog.jellyfishmix.com
- LICENSE GPL-2.0
前言
本文将会根据以下顺序进行叙述:
-
lambda 表达式中传入的局部变量,为什么需要是不可变的(final)?
-
函数式编程提倡的无状态。
-
无状态服务。
lambda 表达式中传入的局部变量,为什么需要是不可变的(final)?
场景演示
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 修饰),或效果上与常量相当的(声明赋初值后就没有被修改过的变量),在编译环节禁止了这种风险。
解决方案
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
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 表达式中出现的外部局部变量,变成了此私有方法的入参。
根据实验得出结论:
- lambda 表达式是一个语法糖,在编译后会生成一个当前类的私有方法。
- lambda 表达式内直接引用局部变量,本质是一种隐式传参,编译时会自动将引用的局部变量,放到根据 lambda 表达式生成的私有方法的参数列表中。
javac 编译器中的"常量折叠"现象
还有一点疑惑,.java 文件的 thread 中的 lambda 表达式,引用了一个常量。这个常量,为什么没体现在根据 lambda 表达式生成的私有方法 lambda$main$0();
的参数列表中呢?
我们来看一下 IDEA 为我们反编译出的更直观的 .class 文件:
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
总结一下自己阅读讨论后的理解:
- 无状态编程在并发编程场景下优势明显,可变状态是多并发编程的天敌,会引发并发编程常见的竟态问题。如果默认情况下值是不可变的,开发者就无需担心某个线程会改变多个线程之间共享变量的值,因此不可变的值消除了竟态条件。由于没有竟态条件,所以不需要使用锁,因此不可变性也消除了与死锁相关的问题。
- 纯函数更容易测试和调试。
比较简单的无状态实现方式,final 修饰变量,把变量标记为不可变。比如 String 类的设计,value 属性加了 fianl 修饰,这让 String 天然就是线程安全的。
无状态服务
- 无状态就是指不能把例如用户登录信息这种数据,放在服务集群中的某台实例机器上,这样其他机器是感知不到这个数据的,只有这台机器自己能感知到。
- 用户的不同操作,都会给这个服务集群发请求,到底哪台机器处理这个用户的本次请求,是不确定的。
- 所以用户登录信息,必须得放在一个服务集群中任何一台机器都能感知到的地方,比如集中存储到 redis 中。
- 这样就把"用户登录信息"这个状态,从 单台机器 移动到了 集中式存储数据源,单台机器就是无状态的了。
- 一个服务集群中,每台机器运行的是同样的代码,此时称这个服务为无状态服务。