02 | 代码加锁:不要让“锁”事成为烦心事
02 | 代码加锁:不要让“锁”事成为烦心事
讲述:王少泽
时长17:20大小15.88M
加锁前要清楚锁和被保护的对象是不是一个层面的
加锁要考虑锁的粒度和场景问题
多把锁要小心死锁问题
重点回顾
思考与讨论
赞 49
提建议
精选留言(57)
- Darren置顶2020-03-10思考与讨论: volatile的问题:可见性问题和禁止指令重排序优化。 可见性问题:本质上是cpu缓存失效,必须从主内存读取数据; 禁止指令重排序优化:x86处理器仅下,只实现了volatile的读写内存屏障,也就是store load,也就是写读,本质上也就是读写可见性,happen-before原则。 实现原理是通过寄存器esp实现的。 当然也不会退出循环,因为cpu缓存到主内存的同步不是实时的。 锁释放和重复执行问题:锁建议使用synchronized,在JDK1.6后,synchronized与Lock性能上差距很小了(优化了很多,自旋锁,自适应自旋锁、偏向锁,轻量级锁等),synchronized也不用程序获取和释放锁,同步代码块是通过monitorenter monitorexit实现的,同步方法是方法头中有ACC_SYNCHRONIZED标志;在分布式场景下,可以考虑etcd,etcd支持锁的自动续期等; 重复执行:首先在锁的使用场景下做好处理,尽量避免重复执行,但业务层面一定要做好幂等。展开
作者回复: 👍🏻
86 - Seven.Lin澤耿2020-03-101.加群解锁没有配对可以用一些代码质量工具协助排插,如Sonar,集成到ide和代码仓库,在编码阶段发现,加上超时自动释放,避免长期占有锁 2.锁超时自动释放导致重复执行的话,可以用锁续期,如redisson的watchdog;或者保证业务的幂等性,重复执行也没问题。
作者回复: 这个回答太赞了!
共 3 条评论56 - 睿睿睿睿睿睿、2020-03-11老师我有个意见代码能否不要大量使用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的语法,帮助你理解
共 9 条评论49 - 黄海峰2020-03-10超时自动释放锁后怎么避免重复逻辑好难,面试曾被卡,求解。。。
作者回复: 有两个方面:1. 避免超时,单独开一个线程给锁延长有效期。比如设置锁有效期30s,有个线程每隔10s重新设置下锁的有效期。 2. 避免重复,业务上增加一个标记是否被处理的字段。或者开一张新表,保存已经处理过的流水号。
共 7 条评论38 - 编程界的小学生2020-03-091.不能退出。必须加volatile,因为volatile保证了可见性。改完后会强制让工作内存失效。去主存拿。如果不加volatile的话那么在while true里面添加输出语句也是OK的。因为println源码加锁了,sync会让当前线程的工作内存失效。 解释的对吗?献丑了。
作者回复: 嗯必须加volatile或者使用AtomicBoolean/AtomicReference等也行,后者相比volatile除了确保可见性还提供了CAS方法保证原子性
共 4 条评论24 - 汤杰2020-03-11对着代码看锁过期蒙了半天,还以为trylock的时间不是等待锁的时间,以为我一直理解的是错误的。最好加上特定的条件。本地锁哪有锁过期呢。原来有些分布式锁为了防止调用方挂了不释放锁加了超时。看到有说用客户端续期的,业务保证的,业务的确一定要保证的,用分布式锁可以解决业务数据库幂等在高并发冲突强烈下性能降低。
作者回复: 抱歉,因为本文删除了原来有的分布式锁的例子,所以最后总结这边的描述谈到的『锁自动超时释放问题』有点唐突,我们改一下。你理解的没错,锁过期是指分布式锁的过期,本地锁是只有等待锁超时
13 - insight2020-03-11看老师使用Lambda表达式感觉学到了非常多,非常支持老师这样做,毕竟程序员就是要不断走出舒适区,学习新东西的。就是老师的Lambda加餐能不能早一点来,对照起来看的更舒服一些
作者回复: 应该快了,大概是下周
12 - 郑思雨2020-08-07一、加锁和释放没有配对: 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、数据库层面:如果有插入操作,建议设置唯一索引,在数据库层面能增加一层安全保障;展开
作者回复: 赞
共 2 条评论10 - 看不到de颜色2020-03-25关于锁过期问题。以前做redis分布式锁的时候一直在思考这个问题。当时觉得就是尽量让锁过期时间比程序执行之间略长一些,以保证加锁区域代码能尽量执行完成。看到老师给其他同学评论说可以用另外一个线程去不断重置锁时间,这里有我理解是针对像redis这种利用setnx实现的分布式锁可以这么解决。那还有其他场景吗?
作者回复: 就是锁续期解决 可以看一下redisson实现
共 3 条评论6 - pedro2020-03-10volatile 老生长谈的问题了,关于锁过期,如果开启一个线程续期,但是有最大重试次数,比如 5 次,那么 5 次以后如何保证其它线程拿到锁而不会重复执行业务了?
作者回复: 可以无限续期,比如redisson的RedissonLock,锁续期是每次续一段时间,比如30秒,然后10秒执行一次续期,虽然是无限次续期,即使客户端崩溃了也没关系无法自动续期后自然会超时
共 3 条评论6 - 木槿花开2020-03-17老师好: //不涉及共享资源的慢方法 TimeUnit.MILLISECONDS.sleep(10) 这个方法本质调用的是Thread的 public static void sleep(long millis, int nanos) 这是一个static的类方法,在加锁粒度太粗的wrong方法中,是不是因为线程都去抢Thread的类锁才导致耗时较长,Thread类不就成公共资源了?
作者回复: 不是这个原因,主要是演示锁包裹了并不需要加锁的慢方法
3 - better2020-03-14实践了一遍add()方法和compare()方法这个例子,结合synchronized的底层原理,不知道理解得正不正确。在这个例子中的理解是,由于synchronized底层是基于moniter指令和对象锁实现的,所以当为add()方法和compare()方法同时加锁后,interesting这个对象就作为对象锁被锁住了,而每次拿到这个对象锁的线程只能有一个,所以执行add()方法的线程和执行compare()方法的两个线程在同一时刻有且只能有一个线程拿到了interesting这个对象锁,所以两个方法就变成串行化执行了,线程安全问题也就得到了解决。 不知道老师结合synchronized底层原理,在这个示例中有没有更好的理解方式展开
作者回复: 没错,主要就是为了防止多个线程交错执行两个方法,synchronized是比较简单清理的解决方式。
3 - 小胡子2020-08-09真实的业务场景中商品成千上万存储在db没办法对商品排序操作,那怎么避免死锁了呢
作者回复: 排序的是要加锁的对象不是数据库所有数据
2 - z小俊、Arno2020-03-26.filter(result -> result) 老师 这个是什么作用啊? 去掉,测试代码错误的示例结果也是对的了。
作者回复: 你可能搞混了,比较下下面三段代码的输出: System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).filter(result->result==true).collect(Collectors.toList())); System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).filter(result->result).collect(Collectors.toList())); System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).map(result->result).collect(Collectors.toList()));
共 2 条评论2 - 何岸康2020-03-17Volatile保证了多线程操作时变量的可见性。java中还有两个关键字可以实现可见性,即synchronized和final。 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html中有比较详细的描述;英语不好我就不翻译了,直接贴出来。 Volatile: “Volatile fields are special fields which are used for communicating state between threads. Each read of a volatile will see the last write to that volatile by any thread; in effect, they are designated by the programmer as fields for which it is never acceptable to see a "stale" value as a result of caching or reordering. The compiler and runtime are prohibited from allocating them in registers. They must also ensure that after they are written, they are flushed out of the cache to main memory, so they can immediately become visible to other threads. Similarly, before a volatile field is read, the cache must be invalidated so that the value in main memory, not the local processor cache, is the one seen. There are also additional restrictions on reordering accesses to volatile variables.” synchronized: “This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.” final: “The values for an object's final fields are set in its constructor. Assuming the object is constructed "correctly", once an object is constructed, the values assigned to the final fields in the constructor will be visible to all other threads without synchronization. In addition, the visible values for any other object or array referenced by those final fields will be at least as up-to-date as the final fields.”展开
作者回复: 不错
2 - mgs20022020-03-16老师,我知道volatile是保证线程间变量可见性和防止指令重排序的,不过开头例子我把a和b的volatile修饰去掉执行right方法,结果也是正确的呢
作者回复: synchronized也可以确保可见性,本例的right实现中volatile不是必须的,我这里是一个例子就没有去掉了,而且主要还为了引出思考题
共 2 条评论2 - 请叫我和尚2020-03-15老师的 Synchronized Demo2 给counter++,right 的方法是给 static 变量加Synchronized,但是上面又说了是多线程去执行++方法,static 是属于类的,你给static 加同步,是否可以理解为你给类加了锁,多线程下去执行这个方法,是不是就变为了不是多线程去并行执行,而是串行去执行了 同理是不是可以直接写 Synchronized(Data.class){ TODO }, 一个实例,比如 right 方法要处理100 个添加操作,本来是多线程去并行做 100N 个添加操作,但是去给 right方法 里加了类锁,就变成了串行去执行,展开
作者回复: 可以给class加锁,对本例效果一样。正如你说的,加锁让让并行变为了串行,所以尽量要锁范围小一点,本例只是demo,真实情况对于++这种操作,加锁就没必要了,用Atomic类更好。
2 - aoe2020-03-09老师,是用Data.class加锁,结果也是正确的。 请教一下:private static Object locker = new Object(); 与Data.class在加锁时,有什么优势吗? 因为我觉得:Data.class相对于新创建的locker对象更节省内存。 public void right1() { synchronized (Data.class) { counter++; } } 测试代码 public static void main(String[] args) throws InterruptedException { LongAdder adder = new LongAdder(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { Data data = new Data(); for (int j = 0; j < 10000; j++) { // data.wrong(); data.right1(); } adder.increment(); } }); thread.start(); } while (adder.longValue() < 10){ Thread.sleep(1000L); } System.out.println(counter); }展开
作者回复: 结果是一样的。但是如果不同方法需要不同的锁,那么就只能用多个锁字段了。
共 2 条评论2 - 西街恶人2020-03-09看老师使用了Lombok2
- Sam.张朝2020-06-05这里都是大神啊,除了我这个小兵1