Java 如何做并发控制
在日常办公或刷短视频时,我们常会遇到“点一次按钮却触发两次操作”的情况——比如抢优惠券时手滑点了两次,结果订单生成了两张;或者多人同时编辑在线文档,最后保存时内容被覆盖。这种“多人同时操作同一份资源出乱子”的问题,放到代码世界里就是并发问题。作为编程语言里的“老牌选手”,Java早就准备了一套实用的并发控制方案,咱们用生活化的例子慢慢讲明白。
最基础的“排队规则”:synchronized
假设公司茶水间只有1台咖啡机,每次只能煮1杯咖啡,总不能让好几个人同时挤过去抢着操作吧?这时候就需要一个“排队规则”,保证同一时间只有一个人用咖啡机。在Java里,synchronized就是干这个的——它相当于给需要共享的“资源”(比如变量、方法、代码块)上了一把“专属锁”,同一时间只能有一个线程能拿到这把锁,其他线程只能乖乖等着。

举个实际的例子:我们写一个模拟抢奶茶店优惠券的程序,优惠券库存只有1张,如果不用synchronized,两个用户同时抢,可能会出现“库存被减了两次”的bug。加上synchronized修饰后,就像给“优惠券库存”上了锁,第一个线程抢到锁后才能减库存,减完释放锁,第二个线程才能继续操作,就不会出错了。不过要注意,这把锁的粒度不能太粗,如果整个方法都加锁,就会像茶水间所有人都等一台咖啡机,效率会变低,最好只给需要共享的代码块加锁。
更灵活的“通行证管理”:显式锁Lock
如果说synchronized是“自动锁”,用完就自动释放,那Lock就是“手动锁”,它给了开发者更多控制权,就像奶茶店给排队的人发“号牌”,没拿到号牌不能操作,还能设置“等待超时”“只等需要的锁”。
比如我们有个场景:多个线程要访问两个共享资源,资源A和资源B。如果用synchronized,线程1拿了资源A的锁,等资源B;线程2拿了资源B的锁,等资源A,就会陷入“互相等待”的死锁。而用Lock的tryLock()方法,能设置“等10秒没拿到锁就放弃”,避免死锁。就像你等奶茶时,等了10分钟还没轮到,就先去拿个快递,不会一直傻等。另外,Lock还能公平排队,让先到的线程先拿锁,避免“后来者插队”。
协调线程的“联络员工具”
光有锁还不够,有时候需要让线程按顺序做事,就像排队打饭时,阿姨会说“你先洗筷子,再拿饭,最后盛汤”。Java提供了几个“协调工具”,帮线程之间传递“信号”。
第一个是CountDownLatch,相当于“倒计时器”,比如公司组织团建,要等所有员工都到齐了才发车,就可以用这个:每个员工到了就减1,倒计时到0,大巴就发车。
第二个是CyclicBarrier,和倒计时器反过来,相当于“集合点”,比如爬山小队要在山脚下集合,所有人到齐了再一起出发,它可以循环用,等所有人走一段路到下一个休息点,还能再等一次。
还有Semaphore,相当于“停车位许可证”,比如停车场只有5个车位,就发5张许可证,每辆车进来拿一张,走了就还,没拿到许可证的车只能在外面等,不会出现“停车场挤爆”的情况,适合控制“有限资源的访问量”。
小心踩坑的“并发陷阱”
最后要提几个常见的坑:比如并发操作的变量如果是普通的,会出现“脏读”,就像你刚把奶茶钱放进收银台,还没确认,另一个人就把钱拿了;还有刚才说的死锁,比如两个线程互相占着资源等,就像两个人过独木桥,都不肯退,结果都卡住了;还有“重复计算”,多个线程同时算同一个任务,结果重复提交,就像几个人同时抢着写同一份报告,最后交了好几份一样的。
其实Java的并发控制,核心就是“管好共享资源的访问”——要么排队,要么协调,要么限制数量,只要把这些工具用对,就能避免大部分并发乱子。不管是日常的生活场景,还是代码里的程序逻辑,“有序、不冲突”都是解决这类问题的核心。