Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

02 | 代码加锁:不要让“锁”事成为烦心事

解决问题:发现和解决加锁和释放没有配对的问题,锁自动释放导致的重复逻辑执行的问题
原因:变量a、b使用了volatile关键字
业务逻辑中有多把锁时要考虑死锁问题
加锁尽可能要考虑粒度和场景
使用synchronized加锁需弄清楚共享资源是类还是实例级别的
避免无限等待和循环等待
下单操作中多个商品的库存锁导致死锁问题
降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁
滥用synchronized会降低性能
非静态字段属于类实例,实例级别的锁可以保护
静态字段属于类,类级别的锁才能保护
为add方法加锁未解决问题
两个int字段a和b的操作导致线程安全问题
介绍了解决线程安全问题的另一种重要手段——锁
使用并发容器等工具解决线程安全的误区
思考与讨论
重点回顾
多把锁要小心死锁问题
加锁要考虑锁的粒度和场景问题
加锁前要清楚锁和被保护的对象是不是一个层面的
有趣案例
误区解析
代码加锁:不要让“锁”事成为烦心事

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。
在上一讲中,我与你介绍了使用并发容器等工具解决线程安全的误区。今天,我们来看看解决线程安全问题的另一种重要手段——锁,在使用上比较容易犯哪些错。
我先和你分享一个有趣的案例吧。有一天,一位同学在群里说“见鬼了,疑似遇到了一个 JVM 的 Bug”,我们都很好奇是什么 Bug。
于是,他贴出了这样一段代码:在一个类里有两个 int 类型的字段 a 和 b,有一个 add 方法循环 1 万次对 a 和 b 进行 ++ 操作,有另一个 compare 方法,同样循环 1 万次判断 a 是否小于 b,条件成立就打印 a 和 b 的值,并判断 a>b 是否成立。
@Slf4j
public class Interesting {
volatile int a = 1;
volatile int b = 1;
public void add() {
log.info("add start");
for (int i = 0; i < 10000; i++) {
a++;
b++;
}
log.info("add done");
}
public void compare() {
log.info("compare start");
for (int i = 0; i < 10000; i++) {
//a始终等于b吗?
if (a < b) {
log.info("a:{},b:{},{}", a, b, a > b);
//最后的a>b应该始终是false吗?
}
}
log.info("compare done");
}
}
他起了两个线程来分别执行 add 和 compare 方法:
Interesting interesting = new Interesting();
new Thread(() -> interesting.add()).start();
new Thread(() -> interesting.compare()).start();
按道理,a 和 b 同样进行累加操作,应该始终相等,compare 中的第一次判断应该始终不会成立,不会输出任何日志。但,执行代码后发现不但输出了日志,而且更诡异的是,compare 方法在判断 a<b 成立的情况下还输出了 a>b 也成立:
群里一位同学看到这个问题笑了,说:“这哪是 JVM 的 Bug,分明是线程安全问题嘛。很明显,你这是在操作两个字段 a 和 b,有线程安全问题,应该为 add 方法加上锁,确保 a 和 b 的 ++ 是原子性的,就不会错乱了。”随后,他为 add 方法加上了锁:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了多线程编程中使用锁解决线程安全问题的重要性和注意事项。作者通过具体案例和代码分析,指出了常见的误区和错误做法。首先,作者分享了一个有趣的案例,展示了在操作多个字段时出现的线程安全问题,以及为方法加锁未能解决问题的原因。其次,作者提到了另一种常见错误,即没有理清锁和被保护对象的层面关系,导致无效的加锁操作。最后,作者强调了加锁时需要考虑锁的粒度和场景问题,避免滥用synchronized关键字,以及在需要保护共享资源时降低锁的粒度。此外,作者还提到了区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁的建议。总的来说,本文通过具体案例和代码分析,深入浅出地介绍了多线程编程中使用锁解决线程安全问题的重要性和注意事项。文章内容丰富,对于需要解决多线程编程中的线程安全问题的读者具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(59)

  • 最新
  • 精选
  • Darren
    置顶
    思考与讨论: volatile的问题:可见性问题和禁止指令重排序优化。 可见性问题:本质上是cpu缓存失效,必须从主内存读取数据; 禁止指令重排序优化:x86处理器仅下,只实现了volatile的读写内存屏障,也就是store load,也就是写读,本质上也就是读写可见性,happen-before原则。 实现原理是通过寄存器esp实现的。 当然也不会退出循环,因为cpu缓存到主内存的同步不是实时的。 锁释放和重复执行问题:锁建议使用synchronized,在JDK1.6后,synchronized与Lock性能上差距很小了(优化了很多,自旋锁,自适应自旋锁、偏向锁,轻量级锁等),synchronized也不用程序获取和释放锁,同步代码块是通过monitorenter monitorexit实现的,同步方法是方法头中有ACC_SYNCHRONIZED标志;在分布式场景下,可以考虑etcd,etcd支持锁的自动续期等; 重复执行:首先在锁的使用场景下做好处理,尽量避免重复执行,但业务层面一定要做好幂等。

    作者回复: 👍🏻

    2020-03-10
    90
  • Seven.Lin澤耿
    1.加群解锁没有配对可以用一些代码质量工具协助排插,如Sonar,集成到ide和代码仓库,在编码阶段发现,加上超时自动释放,避免长期占有锁 2.锁超时自动释放导致重复执行的话,可以用锁续期,如redisson的watchdog;或者保证业务的幂等性,重复执行也没问题。

    作者回复: 这个回答太赞了!

    2020-03-10
    3
    61
  • 睿睿睿睿睿睿、
    老师我有个意见代码能否不要大量使用Lambda表达式,并不是每个读者都是老司机

    作者回复: 其实Java8出来已经挺久了,使用Lambda和Stream可以显著改善代码可读性,确保代码简洁性,因此专栏是大量使用Java8的一些新特性的。给你几个建议: 1、可以进一步订阅极客时间专门的学习java的专栏系统学习Lambda语法,比如https://time.geekbang.org/course/detail/181-107395,然后自己对着练习一下 2、买一本《Java实战第二版》系统学习Java8的方方面面 3、关注一下本专栏的加餐,之后我们会通过加餐介绍下Java8 4、遇到实在看不懂的代码,下载源码后,在IDEA中点击lambda或stream API的地方,停留一下,左侧可以看到有提示 replace stream API with loop或replace lambda with anonymous class选项,翻译为非stream和lambda的语法,帮助你理解

    2020-03-11
    10
    51
  • 黄海峰
    超时自动释放锁后怎么避免重复逻辑好难,面试曾被卡,求解。。。

    作者回复: 有两个方面:1. 避免超时,单独开一个线程给锁延长有效期。比如设置锁有效期30s,有个线程每隔10s重新设置下锁的有效期。 2. 避免重复,业务上增加一个标记是否被处理的字段。或者开一张新表,保存已经处理过的流水号。

    2020-03-10
    7
    39
  • 编程界的小学生
    1.不能退出。必须加volatile,因为volatile保证了可见性。改完后会强制让工作内存失效。去主存拿。如果不加volatile的话那么在while true里面添加输出语句也是OK的。因为println源码加锁了,sync会让当前线程的工作内存失效。 解释的对吗?献丑了。

    作者回复: 嗯必须加volatile或者使用AtomicBoolean/AtomicReference等也行,后者相比volatile除了确保可见性还提供了CAS方法保证原子性

    2020-03-09
    4
    25
  • 汤杰
    对着代码看锁过期蒙了半天,还以为trylock的时间不是等待锁的时间,以为我一直理解的是错误的。最好加上特定的条件。本地锁哪有锁过期呢。原来有些分布式锁为了防止调用方挂了不释放锁加了超时。看到有说用客户端续期的,业务保证的,业务的确一定要保证的,用分布式锁可以解决业务数据库幂等在高并发冲突强烈下性能降低。

    作者回复: 抱歉,因为本文删除了原来有的分布式锁的例子,所以最后总结这边的描述谈到的『锁自动超时释放问题』有点唐突,我们改一下。你理解的没错,锁过期是指分布式锁的过期,本地锁是只有等待锁超时

    2020-03-11
    13
  • insight
    看老师使用Lambda表达式感觉学到了非常多,非常支持老师这样做,毕竟程序员就是要不断走出舒适区,学习新东西的。就是老师的Lambda加餐能不能早一点来,对照起来看的更舒服一些

    作者回复: 应该快了,大概是下周

    2020-03-11
    12
  • 郑思雨
    一、加锁和释放没有配对: lock 与 unlock 通常结对使用,使用时,一般将unlock放在finally代码块中。但是释放锁时最好增加判断: if (lock.isHeldByCurrentThread()) lock.unlock(); 这样避免锁持有超时后释放引发IllegalMonitorStateException异常。 如果怕忘记释放锁,可以将锁封装成一个代理模式,如下: public class AutoUnlockProxy implements Closeable { private Lock lock; public AutoUnlockProxy(Lock lock){ this.lock = lock; } public void lock(){ lock.lock(); } public boolean tryLock(){ return lock.tryLock(); } @Override public void close() throws IOException { lock.unlock(); } } 使用时,通过try-with-resource 的方式使用,可以达到自动释放锁的目的: try(AutoUnlockProxy proxy = new AutoUnlockProxy(new ReentrantLock())){ proxy.lock(); }catch (Exception e){ e.printStackTrace(); } 二、锁自动释放导致的重复逻辑执行(补充的细节点) 1、代码层面:对请求进行验重; 2、数据库层面:如果有插入操作,建议设置唯一索引,在数据库层面能增加一层安全保障;

    作者回复: 赞

    2020-08-07
    2
    11
  • 看不到de颜色
    关于锁过期问题。以前做redis分布式锁的时候一直在思考这个问题。当时觉得就是尽量让锁过期时间比程序执行之间略长一些,以保证加锁区域代码能尽量执行完成。看到老师给其他同学评论说可以用另外一个线程去不断重置锁时间,这里有我理解是针对像redis这种利用setnx实现的分布式锁可以这么解决。那还有其他场景吗?

    作者回复: 就是锁续期解决 可以看一下redisson实现

    2020-03-25
    3
    6
  • pedro
    volatile 老生长谈的问题了,关于锁过期,如果开启一个线程续期,但是有最大重试次数,比如 5 次,那么 5 次以后如何保证其它线程拿到锁而不会重复执行业务了?

    作者回复: 可以无限续期,比如redisson的RedissonLock,锁续期是每次续一段时间,比如30秒,然后10秒执行一次续期,虽然是无限次续期,即使客户端崩溃了也没关系无法自动续期后自然会超时

    2020-03-10
    3
    6
收起评论
显示
设置
留言
59
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部