大家好,我是你们的老朋友,一个平时写代码、也爱拿技术解释生活的中年自媒体作者,今天想聊聊“线程安全”,这词听起来很技术,甚至有点高冷,但我先说结论:线程安全一点都不神秘,它本质上是在解决一个特别生活化的问题——当多个人同时操作同一份东西时,怎么保证结果不乱、数据不丢、账不算错。
你可以把“线程”理解成同时干活的人,线程安全,就是多个执行流同时访问同一份可变数据时,程序还能表现得像你预期的那样正确,注意三个关键词:共享、可变、并发,少了任何一个,事情往往都没那么麻烦,怕就怕大家共享同一份数据,这份数据还会变,而且还同时改。
先讲个生活实例,假设你们家有一个共享账本,余额显示1000元,你和爱人同时用手机记账,各自都记了一笔200元支出,你打开账本时看到余额1000,心里算了一下:1000减200等于800,于是准备写回800,几乎同一时间,爱人也看到余额1000,也算出800,也准备写回800,最后账本上显示800,可实际上,你们明明花了两笔200,余额应该是600,那200去哪了?不是被谁偷了,而是被“并发覆盖”了。
这个场景就是典型的“读—改—写”不是原子操作,两个人都先读、再改、再写,中间没有协调,后写的人把先写的人结果覆盖了,生活里类似的事特别多:两个人同时编辑共享表格同一格,最后一个人的内容把前一个人的覆盖;超市最后一瓶水,两个收银台同时卖出去;外卖平台库存显示还有1份,结果两个用户同时下单成功,你说这是不是线程安全问题?本质一模一样。
写代码更是如此,比如一个计数器:
int count = 0; // 多个线程同时执行 count++;
很多人觉得 count++ 就是一行代码,能出什么问题?可它底层大致分成三步:读 count、加一、写回 count,线程A读到0,线程B也读到0;A写回1,B也写回1,两个线程各加一次,理论上应该变成2,结果还是1,你让两个线程各加1000次,最后可能不是2000,而是1800多,这不是玄学,这是并发没有协调。
那怎么解决?常见办法有加锁,synchronized、ReentrantLock;也有原子类,AtomicInteger;数据库里可以用事务、行锁、乐观锁版本号;分布式场景可以用 Redis 原子操作、消息队列串行化处理,办法很多,但我个人最想说的是:别把“线程安全”理解成“到处加锁”。
我见过不少项目,一遇到并发问题,啪一下给整个方法加锁,甚至给整个类加锁,结果呢?数据是安全了,系统也慢成蜗牛了,锁不是护身符,锁是交通规则,线程安全真正难的地方,不是背几个 API,而是识别边界:哪些数据是共享的?哪些是可变的?哪些操作必须串行?哪些其实可以各干各的?
所以我有一个很明确的个人观点:线程安全的第一原则,不是“加锁”,而是“尽量不共享”,能不让多个线程碰同一份数据,就别让它们碰,局部变量天然更安全;不可变对象省心;每个请求拿自己的副本,冲突就少;必须共享时,再把共享范围缩小,把锁粒度控制住,就像公司排班表,如果十个人同时编辑同一张表,肯定容易乱;后来改成每个人只填自己负责的那一行,汇总时用版本号检查冲突,问题立刻少很多,这叫设计上消灭冲突,而不是事后拿锁硬扛。
线程安全也不是让所有人都去卷底层,我的态度是:写业务代码的人至少要有一根弦——这段代码如果两个人同时点,会不会出问题?尤其是扣库存、扣余额、领优惠券、抢名额、计数统计这些场景,单测跑得再绿,也不代表并发下稳,该做并发测试就做,该加监控就加,该记日志就记,别等上线后出现重复扣款、库存超卖、计数偏差,才回头说“当时没想过”。
线程安全不是高冷术语,而是对“同时操作同一份可变数据”这件事保持敬畏,优先用设计减少共享,必须共享就加正确协调,锁别乱加,边界要想清楚,代码如此,生活也一样,十字路口没有红绿灯会乱,但你不能为了安全把全城路都封了,愿大家写代码并发不翻车,家庭账本不丢钱,抢票下单不超卖。
