0赞
赞赏
更多好文
上周三,我盯着监控报警,手抖得差点把咖啡泼在键盘上。
用户登录时,系统突然返回“用户名重复”,但明明没重复。
一查日志——一个static变量在多线程下被改了。
不是代码逻辑错,是static用错了。
这破事,我干过第3次了。
今天不扯理论,只说点血淋淋的实战经验。
一、static:别把它当“全局变量”用
1. static是什么?
static就是“类的”,不是“对象的”。
类加载一次,static变量就初始化一次,所有对象共享。
public class User {
public static int userCount = 0; // 所有User对象共享这个计数器
public User() {
userCount++; // 每创建一个User,计数器+1
}
}
// 测试
User u1 = new User(); // userCount=1
User u2 = new User(); // userCount=2
System.out.println(User.userCount); // 输出2
真实案例:
有个登录接口用static存用户会话ID,结果两个用户同时登录,ID被覆盖——因为所有请求共享同一个static变量。
后果:用户A看到用户B的资料。
解法:用ThreadLocal或request对象存状态,别用static。
2. static的3个致命陷阱
| 陷阱 | 代码示例 | 为什么坑? |
|---|---|---|
| 多线程不安全 | static int counter = 0;多线程下counter++会漏计 | 读-改-写不是原子操作,可能覆盖 |
| 内存泄漏 | static List<User> users = new ArrayList<>();如果List没清空,整个应用生命周期都占内存 | 对象不会被GC回收,内存越用越大 |
| 破坏单例设计 | public static User getInstance() { return new User(); }每次调用都新建对象 | 本意是单例,实际是多例! |
血泪教训:
我们有个日志工具类,用了static存日志级别:public class LogUtil { public static String level = "INFO"; // 错! }结果:A用户设为
DEBUG,B用户看到的也是DEBUG——因为所有用户共享这个level。
正确做法:去掉static,用实例方法。
二、final:别把它当“常量”用(它比你想的更复杂)
1. final是什么?
final就是“锁死的”。
一旦赋值,不能改。
但注意:final修饰对象,对象内容可以改。
final int MAX_AGE = 100; // 基本类型:值不能改
final String name = "Java"; // String:引用不能变,但内容不能改(String不可变)
final List<String> list = new ArrayList<>(); // 引用不能变,但list内容可以改
list.add("Hello"); // ✅ 可以
list = new ArrayList<>(); // ❌ 不能,编译报错
真实案例:
有个配置类,用final存数据库连接:public class DBConfig { public final Connection conn = DriverManager.getConnection(...); // 错! }结果:多个线程同时用
conn,连接池被搞崩了——因为final只锁了引用,没锁连接本身。
解法:用final+synchronized,或直接用连接池工具。
2. final的3个常见误解
| 误解 | 真相 | 正确做法 |
|---|---|---|
| “final就是常量” | final只锁引用,不锁内容(如List内容可改) | 用Collections.unmodifiableList() |
| “final方法不能被覆盖” | 正确!但final类不能被继承 | 用final类:public final class Config { ... } |
| “final变量必须初始化” | 必须在声明时或构造器里初始化 | public final int id;在构造器里赋值:this.id = 1; |
血泪教训:
有个团队用final存用户权限:public class User { public final Map<String, Boolean> permissions = new HashMap<>(); }结果:用户A加了权限,用户B也看到了——因为
permissions的引用是final,但内容可以改。
正确做法:public final Map<String, Boolean> permissions = Collections.unmodifiableMap(new HashMap<>());
三、static vs final:别混为一谈
| 场景 | 用static | 用final | 为什么? |
|---|---|---|---|
| 工具类方法 | public static void log(String msg) | ❌ 不用final(方法可以覆盖) | 工具类方法不需要覆盖,但不用final也能用 |
| 常量定义 | public static final int MAX = 100; | ✅ 用final(必须) | 常量必须用final,static是附加 |
| 对象状态 | ❌ 别用static存状态(多线程危险) | ✅ 用final存不可变状态 | final保证状态不被篡改 |
| 单例模式 | public static User getInstance() | ❌ 不用final(单例是对象) | 单例是对象,不是常量 |
关键区别:
static→ 类级别(类加载一次,所有对象共享)final→ 值级别(值一旦定,不能改)
别把它们混在一起:public static final int MAX_AGE = 100; // 正确:常量 public static int userCount = 0; // 错误:不该用static存状态
四、避坑清单:我踩过的坑(别再犯)
-
用static存用户状态 → 多线程下数据覆盖。
解法:用ThreadLocal或request对象。 -
用final存List/Map → 内容可改,导致逻辑混乱。
解法:Collections.unmodifiableList(list)。 -
把工具类方法设为final → 无法扩展(比如想在子类里改日志格式)。
解法:工具类用static,但不用final。 -
忘了初始化final变量 → 编译报错。
解法:在声明时初始化,或在构造器里赋值。 -
以为final锁了整个对象 → 实际只锁了引用。
解法:用Collections.unmodifiableXXX()。
最后一句大实话
static不是全局变量,final不是常量——它们都是“工具”,不是“银弹”。
别为了“代码干净”滥用static,也别以为final就能解决所有问题。
上周那个用户状态覆盖的坑,我修了2小时——
不是代码写得烂,是static和final用错了。
行动建议:
- 新项目:能用
final的变量,必须用final(比如常量、配置)。 - 老代码:全搜
public static,看看是不是该用实例变量。 - 用
final存集合?加Collections.unmodifiableXXX()。 - 工具类方法?用
static,但别加final(除非你100%确定不扩展)。
别让static和final在你手里变成“坑”。
—— 一个被“常量”逼到崩溃的Java老手
